You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2022/02/11 13:59:16 UTC

[rocketmq-client-csharp] 01/01: Initial commit

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

lizhanhui pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-client-csharp.git

commit e6a6bb970e56cd89f8cc5c1c9771b4536d850d45
Author: ZhanhuiLi <sh...@alibaba-inc.com>
AuthorDate: Fri Feb 11 21:57:45 2022 +0800

    Initial commit
---
 .gitignore                                         |   2 +
 README.md                                          |   3 +
 examples/Program.cs                                |  12 +
 examples/examples.csproj                           |  12 +
 rocketmq-client-csharp/Class1.cs                   |   8 +
 .../Protos/apache/rocketmq/v1/admin.proto          |  45 ++
 .../Protos/apache/rocketmq/v1/definition.proto     | 351 ++++++++++++++
 .../Protos/apache/rocketmq/v1/service.proto        | 522 +++++++++++++++++++++
 .../Protos/google/rpc/error_details.proto          | 249 ++++++++++
 .../Protos/google/rpc/status.proto                 |  47 ++
 .../rocketmq-client-csharp.csproj                  |  31 ++
 rocketmq-client.sln                                |  62 +++
 tests/UnitTest1.cs                                 |  13 +
 tests/tests.csproj                                 |  20 +
 14 files changed, 1377 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8d4a6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+bin
+obj
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..85a3d46
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+Introduction
+--------------
+Project rocketmq-client-csharp is targeted to implement C# binding in native C# code.
\ No newline at end of file
diff --git a/examples/Program.cs b/examples/Program.cs
new file mode 100644
index 0000000..9d460cf
--- /dev/null
+++ b/examples/Program.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace examples
+{
+    class Program
+    {
+        static void Main(string[] args)
+        {
+            Console.WriteLine("Hello World!");
+        }
+    }
+}
diff --git a/examples/examples.csproj b/examples/examples.csproj
new file mode 100644
index 0000000..03cc409
--- /dev/null
+++ b/examples/examples.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <ItemGroup>
+    <ProjectReference Include="..\rocketmq-client-csharp\rocketmq-client-csharp.csproj" />
+  </ItemGroup>
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net5.0</TargetFramework>
+  </PropertyGroup>
+
+</Project>
diff --git a/rocketmq-client-csharp/Class1.cs b/rocketmq-client-csharp/Class1.cs
new file mode 100644
index 0000000..b9cf30a
--- /dev/null
+++ b/rocketmq-client-csharp/Class1.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace rocketmq_client_csharp
+{
+    public class Class1
+    {
+    }
+}
diff --git a/rocketmq-client-csharp/Protos/apache/rocketmq/v1/admin.proto b/rocketmq-client-csharp/Protos/apache/rocketmq/v1/admin.proto
new file mode 100644
index 0000000..554207b
--- /dev/null
+++ b/rocketmq-client-csharp/Protos/apache/rocketmq/v1/admin.proto
@@ -0,0 +1,45 @@
+// 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.
+
+syntax = "proto3";
+
+package apache.rocketmq.v1;
+
+option cc_enable_arenas = true;
+option java_multiple_files = true;
+option java_package = "apache.rocketmq.v1";
+option java_generate_equals_and_hash = true;
+option java_string_check_utf8 = true;
+option java_outer_classname = "MQAdmin";
+
+message ChangeLogLevelRequest {
+  enum Level {
+    TRACE = 0;
+    DEBUG = 1;
+    INFO = 2;
+    WARN = 3;
+    ERROR = 4;
+  }
+  Level level = 1;
+}
+
+message ChangeLogLevelResponse {
+  string remark = 1;
+}
+
+service Admin {
+  rpc ChangeLogLevel(ChangeLogLevelRequest) returns (ChangeLogLevelResponse) {
+  }
+}
\ No newline at end of file
diff --git a/rocketmq-client-csharp/Protos/apache/rocketmq/v1/definition.proto b/rocketmq-client-csharp/Protos/apache/rocketmq/v1/definition.proto
new file mode 100644
index 0000000..33f4644
--- /dev/null
+++ b/rocketmq-client-csharp/Protos/apache/rocketmq/v1/definition.proto
@@ -0,0 +1,351 @@
+// 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.
+
+syntax = "proto3";
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+
+package apache.rocketmq.v1;
+
+option java_multiple_files = true;
+option java_package = "apache.rocketmq.v1";
+option java_generate_equals_and_hash = true;
+option java_string_check_utf8 = true;
+option java_outer_classname = "MQDomain";
+
+option csharp_namespace = "apache.rocketmq.v1";
+
+enum Permission {
+  NONE = 0;
+  READ = 1;
+  WRITE = 2;
+  READ_WRITE = 3;
+
+  reserved 4 to 64;
+}
+
+enum FilterType {
+  TAG = 0;
+  SQL = 1;
+
+  reserved 2 to 64;
+}
+
+message FilterExpression {
+  FilterType type = 1;
+  string expression = 2;
+
+  reserved 3 to 64;
+}
+
+// Dead lettering is done on a best effort basis. The same message might be
+// dead lettered multiple times.
+//
+// If validation on any of the fields fails at subscription creation/update,
+// the create/update subscription request will fail.
+message DeadLetterPolicy {
+  // The maximum number of delivery attempts for any message.
+  //
+  // This field will be honored on a best effort basis.
+  //
+  // If this parameter is 0, a default value of 16 is used.
+  int32 max_delivery_attempts = 1;
+
+  reserved 2 to 64;
+}
+
+message Resource {
+  string resource_namespace = 1;
+
+  // Resource name identifier, which remains unique within the abstract resource
+  // namespace.
+  string name = 2;
+
+  reserved 3 to 64;
+}
+
+enum ConsumeModel {
+  CLUSTERING = 0;
+  BROADCASTING = 1;
+
+  reserved 2 to 64;
+}
+
+message ProducerData {
+  Resource group = 1;
+
+  reserved 2 to 64;
+}
+
+enum ConsumePolicy {
+  RESUME = 0;
+  PLAYBACK = 1;
+  DISCARD = 2;
+  TARGET_TIMESTAMP = 3;
+
+  reserved 4 to 64;
+}
+
+enum ConsumeMessageType {
+  ACTIVE = 0;
+  PASSIVE = 1;
+
+  reserved 2 to 64;
+}
+
+message ConsumerData {
+  Resource group = 1;
+
+  repeated SubscriptionEntry subscriptions = 2;
+
+  ConsumeModel consume_model = 3;
+
+  ConsumePolicy consume_policy = 4;
+
+  DeadLetterPolicy dead_letter_policy = 5;
+
+  ConsumeMessageType consume_type = 6;
+
+  reserved 7 to 64;
+}
+
+message SubscriptionEntry {
+  Resource topic = 1;
+  FilterExpression expression = 2;
+
+  reserved 3 to 64;
+}
+
+enum AddressScheme {
+  IPv4 = 0;
+  IPv6 = 1;
+  DOMAIN_NAME = 2;
+
+  reserved 3 to 64;
+}
+
+message Address {
+  string host = 1;
+  int32 port = 2;
+
+  reserved 3 to 64;
+}
+
+message Endpoints {
+  AddressScheme scheme = 1;
+  repeated Address addresses = 2;
+
+  reserved 3 to 64;
+}
+
+message Broker {
+  // Name of the broker
+  string name = 1;
+
+  // Broker index. Canonically, index = 0 implies that the broker is playing
+  // leader role while brokers with index > 0 play follower role.
+  int32 id = 2;
+
+  // Address of the broker, complying with the following scheme
+  // 1. dns:[//authority/]host[:port]
+  // 2. ipv4:address[:port][,address[:port],...] – IPv4 addresses
+  // 3. ipv6:address[:port][,address[:port],...] – IPv6 addresses
+  Endpoints endpoints = 3;
+
+  reserved 4 to 64;
+}
+
+message Partition {
+  Resource topic = 1;
+  int32 id = 2;
+  Permission permission = 3;
+  Broker broker = 4;
+
+  reserved 5 to 64;
+}
+
+enum MessageType {
+  NORMAL = 0;
+
+  // Sequenced message
+  FIFO = 1;
+
+  // Messages that are delivered after the specified duration.
+  DELAY = 2;
+
+  // Messages that are transactional. Only committed messages are delivered to
+  // subscribers.
+  TRANSACTION = 3;
+
+  reserved 4 to 64;
+}
+
+enum DigestType {
+  // CRC algorithm achieves goal of detecting random data error with lowest
+  // computation overhead.
+  CRC32 = 0;
+
+  // MD5 algorithm achieves good balance between collision rate and computation
+  // overhead.
+  MD5 = 1;
+
+  // SHA-family has substantially fewer collision with fair amount of
+  // computation.
+  SHA1 = 2;
+
+  reserved 3 to 64;
+}
+
+// When publishing messages to or subscribing messages from brokers, clients
+// shall include or validate digests of message body to ensure data integrity.
+//
+// For message publishment, when an invalid digest were detected, brokers need
+// respond client with BAD_REQUEST.
+//
+// For messags subscription, when an invalid digest were detected, consumers
+// need to handle this case according to message type:
+// 1) Standard messages should be negatively acknowledged instantly, causing
+// immediate re-delivery; 2) FIFO messages require special RPC, to re-fetch
+// previously acquired messages batch;
+//
+// Message consumption model also affects how invalid digest are handled. When
+// messages are consumed in broadcasting way,
+// TODO: define semantics of invalid-digest-when-broadcasting.
+message Digest {
+  DigestType type = 1;
+  string checksum = 2;
+
+  reserved 3 to 64;
+}
+
+enum Encoding {
+  IDENTITY = 0;
+  GZIP = 1;
+
+  reserved 2 to 64;
+}
+
+message SystemAttribute {
+  // Tag
+  string tag = 1;
+
+  // Message keys
+  repeated string keys = 2;
+
+  // Message identifier, client-side generated, remains unique.
+  // if message_id is empty, the send message request will be aborted with
+  // status `INVALID_ARGUMENT`
+  string message_id = 3;
+
+  // Message body digest
+  Digest body_digest = 4;
+
+  // Message body encoding. Candidate options are identity, gzip, snappy etc.
+  Encoding body_encoding = 5;
+
+  // Message type, normal, FIFO or transactional.
+  MessageType message_type = 6;
+
+  // Message born time-point.
+  google.protobuf.Timestamp born_timestamp = 7;
+
+  // Message born host. Valid options are IPv4, IPv6 or client host domain name.
+  string born_host = 8;
+
+  // Time-point at which the message is stored in the broker.
+  google.protobuf.Timestamp store_timestamp = 9;
+
+  // The broker that stores this message. It may be name, IP or arbitrary
+  // identifier that uniquely identify the broker.
+  string store_host = 10;
+
+  oneof timed_delivery {
+    // Time-point at which broker delivers to clients.
+    google.protobuf.Timestamp delivery_timestamp = 11;
+
+    // Level-based delay strategy.
+    int32 delay_level = 12;
+  }
+
+  // If a message is acquired by way of POP, this field holds the receipt.
+  // Clients use the receipt to acknowledge or negatively acknowledge the
+  // message.
+  string receipt_handle = 13;
+
+  // Partition identifier in which a message is physically stored.
+  int32 partition_id = 14;
+
+  // Partition offset at which a message is stored.
+  int64 partition_offset = 15;
+
+  // Period of time servers would remain invisible once a message is acquired.
+  google.protobuf.Duration invisible_period = 16;
+
+  // Business code may failed to process messages for the moment. Hence, clients
+  // may request servers to deliver them again using certain back-off strategy,
+  // the attempt is 1 not 0 if message is delivered first time.
+  int32 delivery_attempt = 17;
+
+  // Message producer load-balance group if applicable.
+  Resource producer_group = 18;
+
+  string message_group = 19;
+
+  // Trace context.
+  string trace_context = 20;
+
+  // Delay time of first recover orphaned transaction request from server.
+  google.protobuf.Duration orphaned_transaction_recovery_period = 21;
+
+  reserved 22 to 64;
+}
+
+message Message {
+
+  Resource topic = 1;
+
+  // User defined key-value pairs.
+  // If user_attribute contains the reserved keys by RocketMQ,
+  // the send message request will be aborted with status `INVALID_ARGUMENT`.
+  // See below links for the reserved keys
+  // https://github.com/apache/rocketmq/blob/master/common/src/main/java/org/apache/rocketmq/common/message/MessageConst.java#L58
+  map<string, string> user_attribute = 2;
+
+  SystemAttribute system_attribute = 3;
+
+  bytes body = 4;
+
+  reserved 5 to 64;
+}
+
+message Assignment {
+  Partition Partition = 1;
+
+  reserved 2 to 64;
+}
+
+enum QueryOffsetPolicy {
+  // Use this option if client wishes to playback all existing messages.
+  BEGINNING = 0;
+
+  // Use this option if client wishes to skip all existing messages.
+  END = 1;
+
+  // Use this option if time-based seek is targeted.
+  TIME_POINT = 2;
+
+  reserved 3 to 64;
+}
\ No newline at end of file
diff --git a/rocketmq-client-csharp/Protos/apache/rocketmq/v1/service.proto b/rocketmq-client-csharp/Protos/apache/rocketmq/v1/service.proto
new file mode 100644
index 0000000..2dacfa8
--- /dev/null
+++ b/rocketmq-client-csharp/Protos/apache/rocketmq/v1/service.proto
@@ -0,0 +1,522 @@
+// 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.
+
+syntax = "proto3";
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "google/rpc/error_details.proto";
+import "google/rpc/status.proto";
+
+import "apache/rocketmq/v1/definition.proto";
+
+package apache.rocketmq.v1;
+
+option java_multiple_files = true;
+option java_package = "apache.rocketmq.v1";
+option java_generate_equals_and_hash = true;
+option java_string_check_utf8 = true;
+option java_outer_classname = "MQService";
+
+option csharp_namespace = "apache.rocketmq.v1";
+
+message ResponseCommon {
+  google.rpc.Status status = 1;
+  google.rpc.RequestInfo request_info = 2;
+  google.rpc.Help help = 3;
+  google.rpc.RetryInfo retry_info = 4;
+  google.rpc.DebugInfo debug_info = 5;
+  google.rpc.ErrorInfo error_info = 6;
+
+  reserved 7 to 64;
+}
+
+// Topics are destination of messages to publish to or subscribe from. Similar
+// to domain names, they will be addressable after resolution through the
+// provided access point.
+//
+// Access points are usually the addresses of name servers, which fulfill
+// service discovery, load-balancing and other auxillary services. Name servers
+// receive periodic heartbeats from affiliate brokers and erase those which
+// failed to maintain alive status.
+//
+// Name servers answer queries of QueryRouteRequest, responding clients with
+// addressable partitions, which they may directly publish messages to or
+// subscribe messages from.
+//
+// QueryRouteRequest shall include source endpoints, aka, configured
+// access-point, which annotates tenant-id, instance-id or other
+// vendor-specific settings. Purpose-built name servers may respond customized
+// results based on these particular requirements.
+message QueryRouteRequest {
+  Resource topic = 1;
+
+  Endpoints endpoints = 2;
+
+  reserved 3 to 64;
+}
+
+message QueryRouteResponse {
+  ResponseCommon common = 1;
+
+  repeated Partition partitions = 2;
+
+  reserved 3 to 64;
+}
+
+message SendMessageRequest {
+  Message message = 1;
+  Partition partition = 2;
+
+  reserved 3 to 64;
+}
+
+message SendMessageResponse {
+  ResponseCommon common = 1;
+  string message_id = 2;
+  string transaction_id = 3;
+
+  reserved 4 to 64;
+}
+
+message QueryAssignmentRequest {
+  Resource topic = 1;
+  Resource group = 2;
+  string client_id = 3;
+
+  // Service access point
+  Endpoints endpoints = 4;
+
+  reserved 5 to 64;
+}
+
+message QueryAssignmentResponse {
+  ResponseCommon common = 1;
+  repeated Assignment assignments = 2;
+
+  reserved 3 to 64;
+}
+
+message ReceiveMessageRequest {
+  Resource group = 1;
+  string client_id = 2;
+  Partition partition = 3;
+  FilterExpression filter_expression = 4;
+  ConsumePolicy consume_policy = 5;
+  google.protobuf.Timestamp initialization_timestamp = 6;
+  int32 batch_size = 7;
+  google.protobuf.Duration invisible_duration = 8;
+  google.protobuf.Duration await_time = 9;
+  bool fifo_flag = 10;
+
+  reserved 11 to 64;
+}
+
+message ReceiveMessageResponse {
+  ResponseCommon common = 1;
+  repeated Message messages = 2;
+  google.protobuf.Timestamp delivery_timestamp = 3;
+  google.protobuf.Duration invisible_duration = 4;
+
+  reserved 5 to 64;
+}
+
+message AckMessageRequest {
+  Resource group = 1;
+  Resource topic = 2;
+  string client_id = 3;
+  oneof handle {
+    string receipt_handle = 4;
+    int64 offset = 5;
+  }
+  string message_id = 6;
+
+  reserved 7 to 64;
+}
+
+message AckMessageResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message NackMessageRequest {
+  Resource group = 1;
+  Resource topic = 2;
+  string client_id = 3;
+  string receipt_handle = 4;
+  string message_id = 5;
+  int32 delivery_attempt = 6;
+  int32 max_delivery_attempts = 7;
+
+  reserved 8 to 64;
+}
+
+message NackMessageResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message ForwardMessageToDeadLetterQueueRequest {
+  Resource group = 1;
+  Resource topic = 2;
+  string client_id = 3;
+  string receipt_handle = 4;
+  string message_id = 5;
+  int32 delivery_attempt = 6;
+  int32 max_delivery_attempts = 7;
+
+  reserved 8 to 64;
+}
+
+message ForwardMessageToDeadLetterQueueResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message HeartbeatRequest {
+  string client_id = 1;
+  oneof client_data {
+    ProducerData producer_data = 2;
+    ConsumerData consumer_data = 3;
+  }
+  bool fifo_flag = 4;
+
+  reserved 5 to 64;
+}
+
+message HeartbeatResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message HealthCheckRequest {
+  Resource group = 1;
+  string client_host = 2;
+
+  reserved 3 to 64;
+}
+
+message HealthCheckResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message EndTransactionRequest {
+  Resource group = 1;
+  string message_id = 2;
+  string transaction_id = 3;
+  enum TransactionResolution {
+    COMMIT = 0;
+    ROLLBACK = 1;
+  }
+  TransactionResolution resolution = 4;
+  enum Source {
+    CLIENT = 0;
+    SERVER_CHECK = 1;
+  }
+  Source source = 5;
+  string trace_context = 6;
+
+  reserved 7 to 64;
+}
+
+message EndTransactionResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message QueryOffsetRequest {
+  Partition partition = 1;
+  QueryOffsetPolicy policy = 2;
+  google.protobuf.Timestamp time_point = 3;
+
+  reserved 4 to 64;
+}
+
+message QueryOffsetResponse {
+  ResponseCommon common = 1;
+  int64 offset = 2;
+
+  reserved 3 to 64;
+}
+
+message PullMessageRequest {
+  Resource group = 1;
+  Partition partition = 2;
+  int64 offset = 3;
+  int32 batch_size = 4;
+  google.protobuf.Duration await_time = 5;
+  FilterExpression filter_expression = 6;
+  string client_id = 7;
+
+  reserved 8 to 64;
+}
+
+message PullMessageResponse {
+  ResponseCommon common = 1;
+  int64 min_offset = 2;
+  int64 next_offset = 3;
+  int64 max_offset = 4;
+  repeated Message messages = 5;
+
+  reserved 6 to 64;
+}
+
+message NoopCommand { reserved 1 to 64; }
+
+message PrintThreadStackTraceCommand {
+  string command_id = 1;
+
+  reserved 2 to 64;
+}
+
+message ReportThreadStackTraceRequest {
+  string command_id = 1;
+  string thread_stack_trace = 2;
+
+  reserved 3 to 64;
+}
+
+message ReportThreadStackTraceResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message VerifyMessageConsumptionCommand {
+  string command_id = 1;
+  Message message = 2;
+
+  reserved 3 to 64;
+}
+
+message ReportMessageConsumptionResultRequest {
+  string command_id = 1;
+
+  // 1. Return `INVALID_ARGUMENT` if message is corrupted.
+  // 2. Return `INTERNAL` if failed to consume message.
+  // 3. Return `OK` if success.
+  google.rpc.Status status = 2;
+
+  reserved 3 to 64;
+}
+
+message ReportMessageConsumptionResultResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+message RecoverOrphanedTransactionCommand {
+  Message orphaned_transactional_message = 1;
+  string transaction_id = 2;
+
+  reserved 3 to 64;
+}
+
+message PollCommandRequest {
+  string client_id = 1;
+  repeated Resource topics = 2;
+  oneof group {
+    Resource producer_group = 3;
+    Resource consumer_group = 4;
+  }
+
+  reserved 5 to 64;
+}
+
+message PollCommandResponse {
+  oneof type {
+    // Default command when no new command need to be delivered.
+    NoopCommand noop_command = 1;
+    // Request client to print thread stack trace.
+    PrintThreadStackTraceCommand print_thread_stack_trace_command = 2;
+    // Request client to verify the consumption of the appointed message.
+    VerifyMessageConsumptionCommand verify_message_consumption_command = 3;
+    // Request client to recover the orphaned transaction message.
+    RecoverOrphanedTransactionCommand recover_orphaned_transaction_command = 4;
+  }
+
+  reserved 5 to 64;
+}
+
+message NotifyClientTerminationRequest {
+  oneof group {
+    Resource producer_group = 1;
+    Resource consumer_group = 2;
+  }
+  string client_id = 3;
+
+  reserved 4 to 64;
+}
+
+message NotifyClientTerminationResponse {
+  ResponseCommon common = 1;
+
+  reserved 2 to 64;
+}
+
+// For all the RPCs in MessagingService, the following error handling policies
+// apply:
+//
+// If the request doesn't bear a valid authentication credential, return a
+// response with common.status.code == `UNAUTHENTICATED`. If the authenticated
+// user is not granted with sufficient permission to execute the requested
+// operation, return a response with common.status.code == `PERMISSION_DENIED`.
+// If the per-user-resource-based quota is exhausted, return a response with
+// common.status.code == `RESOURCE_EXHAUSTED`. If any unexpected server-side
+// errors raise, return a response with common.status.code == `INTERNAL`.
+service MessagingService {
+
+  // Querys the route entries of the requested topic in the perspective of the
+  // given endpoints. On success, servers should return a collection of
+  // addressable partitions. Note servers may return customized route entries
+  // based on endpoints provided.
+  //
+  // If the requested topic doesn't exist, returns `NOT_FOUND`.
+  // If the specific endpoints is emtpy, returns `INVALID_ARGUMENT`.
+  rpc QueryRoute(QueryRouteRequest) returns (QueryRouteResponse) {}
+
+  // Producer or consumer sends HeartbeatRequest to servers periodically to
+  // keep-alive. Additionally, it also reports client-side configuration,
+  // including topic subscription, load-balancing group name, etc.
+  //
+  // Returns `OK` if success.
+  //
+  // If a client specifies a language that is not yet supported by servers,
+  // returns `INVALID_ARGUMENT`
+  rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse) {}
+
+  // Checks the health status of message server, returns `OK` if services are
+  // online and serving. Clients may use this RPC to detect availability of
+  // messaging service, and take isolation actions when necessary.
+  rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse) {}
+
+  // Delivers messages to brokers.
+  // Clients may further:
+  // 1. Refine a message destination to topic partition which fulfills parts of
+  // FIFO semantic;
+  // 2. Flag a message as transactional, which keeps it invisible to consumers
+  // until it commits;
+  // 3. Time a message, making it invisible to consumers till specified
+  // time-point;
+  // 4. And more...
+  //
+  // Returns message-id or transaction-id with status `OK` on success.
+  //
+  // If the destination topic doesn't exist, returns `NOT_FOUND`.
+  rpc SendMessage(SendMessageRequest) returns (SendMessageResponse) {}
+
+  // Querys the assigned partition route info of a topic for current consumer,
+  // the returned assignment result is descided by server-side load balacner.
+  //
+  // If the corresponding topic doesn't exist, returns `NOT_FOUND`.
+  // If the specific endpoints is emtpy, returns `INVALID_ARGUMENT`.
+  rpc QueryAssignment(QueryAssignmentRequest)
+      returns (QueryAssignmentResponse) {}
+
+  // Receives messages from the server in batch manner, returns a set of
+  // messages if success. The received messages should be acked or uacked after
+  // processed.
+  //
+  // If the pending concurrent receive requests exceed the quota of the given
+  // consumer group, returns `UNAVAILABLE`. If the upstream store server hangs,
+  // return `DEADLINE_EXCEEDED` in a timely manner. If the corresponding topic
+  // or consumer group doesn't exist, returns `NOT_FOUND`. If there is no new
+  // message in the specific topic, returns `OK` with an empty message set.
+  // Please note that client may suffer from false empty responses.
+  rpc ReceiveMessage(ReceiveMessageRequest) returns (ReceiveMessageResponse) {}
+
+  // Acknowledges the message associated with the `receipt_handle` or `offset`
+  // in the `AckMessageRequest`, it means the message has been successfully
+  // processed. Returns `OK` if the message server remove the relevant message
+  // successfully.
+  //
+  // If the given receipt_handle is illegal or out of date, returns
+  // `INVALID_ARGUMENT`.
+  rpc AckMessage(AckMessageRequest) returns (AckMessageResponse) {}
+
+  // Signals that the message has not been successfully processed. The message
+  // server should resend the message follow the retry policy defined at
+  // server-side.
+  //
+  // If the corresponding topic or consumer group doesn't exist, returns
+  // `NOT_FOUND`.
+  rpc NackMessage(NackMessageRequest) returns (NackMessageResponse) {}
+
+  // Forwards one message to dead letter queue if the DeadLetterPolicy is
+  // triggered by this message at client-side, return `OK` if success.
+  rpc ForwardMessageToDeadLetterQueue(ForwardMessageToDeadLetterQueueRequest)
+      returns (ForwardMessageToDeadLetterQueueResponse) {}
+
+  // Commits or rollback one transactional message.
+  rpc EndTransaction(EndTransactionRequest) returns (EndTransactionResponse) {}
+
+  // Querys the offset of the specific partition, returns the offset with `OK`
+  // if success. The message server should maintain a numerical offset for each
+  // message in a parition.
+  rpc QueryOffset(QueryOffsetRequest) returns (QueryOffsetResponse) {}
+
+  // Pulls messages from the specific partition, returns a set of messages with
+  // next pull offset. The pulled messages can't be acked or nacked, while the
+  // client is responsible for manage offesets for consumer, typically update
+  // consume offset to local memory or a third-party storage service.
+  //
+  // If the pending concurrent receive requests exceed the quota of the given
+  // consumer group, returns `UNAVAILABLE`. If the upstream store server hangs,
+  // return `DEADLINE_EXCEEDED` in a timely manner. If the corresponding topic
+  // or consumer group doesn't exist, returns `NOT_FOUND`. If there is no new
+  // message in the specific topic, returns `OK` with an empty message set.
+  // Please note that client may suffer from false empty responses.
+  rpc PullMessage(PullMessageRequest) returns (PullMessageResponse) {}
+
+  // Multiplexing RPC(s) for various polling requests, which issue different
+  // commands to client.
+  //
+  // Sometimes client may need to receive and process the command from server.
+  // To prevent the complexity of streaming RPC(s), a unary RPC using
+  // long-polling is another solution.
+  //
+  // To mark the request-response of corresponding command, `command_id` in
+  // message is recorded in the subsequent RPC(s). For example, after receiving
+  // command of printing thread stack trace, client would send
+  // `ReportMessageConsumptionResultRequest` to server, which contain both of
+  // the stack trace and `command_id`.
+  //
+  // At same time, `NoopCommand` is delivered from server when no new command is
+  // needed, it is essential for client to maintain the ping-pong.
+  //
+  rpc PollCommand(PollCommandRequest) returns (PollCommandResponse) {}
+
+  // After receiving the corresponding polling command, the thread stack trace
+  // is reported to the server.
+  rpc ReportThreadStackTrace(ReportThreadStackTraceRequest)
+      returns (ReportThreadStackTraceResponse) {}
+
+  // After receiving the corresponding polling command, the consumption result
+  // of appointed message is reported to the server.
+  rpc ReportMessageConsumptionResult(ReportMessageConsumptionResultRequest)
+      returns (ReportMessageConsumptionResultResponse) {}
+
+  // Notify the server that the client is terminated.
+  rpc NotifyClientTermination(NotifyClientTerminationRequest)
+      returns (NotifyClientTerminationResponse) {}
+}
\ No newline at end of file
diff --git a/rocketmq-client-csharp/Protos/google/rpc/error_details.proto b/rocketmq-client-csharp/Protos/google/rpc/error_details.proto
new file mode 100644
index 0000000..c4d6c4b
--- /dev/null
+++ b/rocketmq-client-csharp/Protos/google/rpc/error_details.proto
@@ -0,0 +1,249 @@
+// Copyright 2020 Google LLC
+//
+// Licensed 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.
+
+syntax = "proto3";
+
+package google.rpc;
+
+import "google/protobuf/duration.proto";
+
+option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails";
+option java_multiple_files = true;
+option java_outer_classname = "ErrorDetailsProto";
+option java_package = "com.google.rpc";
+option objc_class_prefix = "RPC";
+
+// Describes when the clients can retry a failed request. Clients could ignore
+// the recommendation here or retry when this information is missing from error
+// responses.
+//
+// It's always recommended that clients should use exponential backoff when
+// retrying.
+//
+// Clients should wait until `retry_delay` amount of time has passed since
+// receiving the error response before retrying.  If retrying requests also
+// fail, clients should use an exponential backoff scheme to gradually increase
+// the delay between retries based on `retry_delay`, until either a maximum
+// number of retries have been reached or a maximum retry delay cap has been
+// reached.
+message RetryInfo {
+  // Clients should wait at least this long between retrying the same request.
+  google.protobuf.Duration retry_delay = 1;
+}
+
+// Describes additional debugging info.
+message DebugInfo {
+  // The stack trace entries indicating where the error occurred.
+  repeated string stack_entries = 1;
+
+  // Additional debugging information provided by the server.
+  string detail = 2;
+}
+
+// Describes how a quota check failed.
+//
+// For example if a daily limit was exceeded for the calling project,
+// a service could respond with a QuotaFailure detail containing the project
+// id and the description of the quota limit that was exceeded.  If the
+// calling project hasn't enabled the service in the developer console, then
+// a service could respond with the project id and set `service_disabled`
+// to true.
+//
+// Also see RetryInfo and Help types for other details about handling a
+// quota failure.
+message QuotaFailure {
+  // A message type used to describe a single quota violation.  For example, a
+  // daily quota or a custom quota that was exceeded.
+  message Violation {
+    // The subject on which the quota check failed.
+    // For example, "clientip:<ip address of client>" or "project:<Google
+    // developer project id>".
+    string subject = 1;
+
+    // A description of how the quota check failed. Clients can use this
+    // description to find more about the quota configuration in the service's
+    // public documentation, or find the relevant quota limit to adjust through
+    // developer console.
+    //
+    // For example: "Service disabled" or "Daily Limit for read operations
+    // exceeded".
+    string description = 2;
+  }
+
+  // Describes all quota violations.
+  repeated Violation violations = 1;
+}
+
+// Describes the cause of the error with structured details.
+//
+// Example of an error when contacting the "pubsub.googleapis.com" API when it
+// is not enabled:
+//
+//     { "reason": "API_DISABLED"
+//       "domain": "googleapis.com"
+//       "metadata": {
+//         "resource": "projects/123",
+//         "service": "pubsub.googleapis.com"
+//       }
+//     }
+//
+// This response indicates that the pubsub.googleapis.com API is not enabled.
+//
+// Example of an error that is returned when attempting to create a Spanner
+// instance in a region that is out of stock:
+//
+//     { "reason": "STOCKOUT"
+//       "domain": "spanner.googleapis.com",
+//       "metadata": {
+//         "availableRegions": "us-central1,us-east2"
+//       }
+//     }
+message ErrorInfo {
+  // The reason of the error. This is a constant value that identifies the
+  // proximate cause of the error. Error reasons are unique within a particular
+  // domain of errors. This should be at most 63 characters and match
+  // /[A-Z0-9_]+/.
+  string reason = 1;
+
+  // The logical grouping to which the "reason" belongs. The error domain
+  // is typically the registered service name of the tool or product that
+  // generates the error. Example: "pubsub.googleapis.com". If the error is
+  // generated by some common infrastructure, the error domain must be a
+  // globally unique value that identifies the infrastructure. For Google API
+  // infrastructure, the error domain is "googleapis.com".
+  string domain = 2;
+
+  // Additional structured details about this error.
+  //
+  // Keys should match /[a-zA-Z0-9-_]/ and be limited to 64 characters in
+  // length. When identifying the current value of an exceeded limit, the units
+  // should be contained in the key, not the value.  For example, rather than
+  // {"instanceLimit": "100/request"}, should be returned as,
+  // {"instanceLimitPerRequest": "100"}, if the client exceeds the number of
+  // instances that can be created in a single (batch) request.
+  map<string, string> metadata = 3;
+}
+
+// Describes what preconditions have failed.
+//
+// For example, if an RPC failed because it required the Terms of Service to be
+// acknowledged, it could list the terms of service violation in the
+// PreconditionFailure message.
+message PreconditionFailure {
+  // A message type used to describe a single precondition failure.
+  message Violation {
+    // The type of PreconditionFailure. We recommend using a service-specific
+    // enum type to define the supported precondition violation subjects. For
+    // example, "TOS" for "Terms of Service violation".
+    string type = 1;
+
+    // The subject, relative to the type, that failed.
+    // For example, "google.com/cloud" relative to the "TOS" type would indicate
+    // which terms of service is being referenced.
+    string subject = 2;
+
+    // A description of how the precondition failed. Developers can use this
+    // description to understand how to fix the failure.
+    //
+    // For example: "Terms of service not accepted".
+    string description = 3;
+  }
+
+  // Describes all precondition violations.
+  repeated Violation violations = 1;
+}
+
+// Describes violations in a client request. This error type focuses on the
+// syntactic aspects of the request.
+message BadRequest {
+  // A message type used to describe a single bad request field.
+  message FieldViolation {
+    // A path leading to a field in the request body. The value will be a
+    // sequence of dot-separated identifiers that identify a protocol buffer
+    // field. E.g., "field_violations.field" would identify this field.
+    string field = 1;
+
+    // A description of why the request element is bad.
+    string description = 2;
+  }
+
+  // Describes all violations in a client request.
+  repeated FieldViolation field_violations = 1;
+}
+
+// Contains metadata about the request that clients can attach when filing a bug
+// or providing other forms of feedback.
+message RequestInfo {
+  // An opaque string that should only be interpreted by the service generating
+  // it. For example, it can be used to identify requests in the service's logs.
+  string request_id = 1;
+
+  // Any data that was used to serve this request. For example, an encrypted
+  // stack trace that can be sent back to the service provider for debugging.
+  string serving_data = 2;
+}
+
+// Describes the resource that is being accessed.
+message ResourceInfo {
+  // A name for the type of resource being accessed, e.g. "sql table",
+  // "cloud storage bucket", "file", "Google calendar"; or the type URL
+  // of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic".
+  string resource_type = 1;
+
+  // The name of the resource being accessed.  For example, a shared calendar
+  // name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current
+  // error is [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED].
+  string resource_name = 2;
+
+  // The owner of the resource (optional).
+  // For example, "user:<owner email>" or "project:<Google developer project
+  // id>".
+  string owner = 3;
+
+  // Describes what error is encountered when accessing this resource.
+  // For example, updating a cloud project may require the `writer` permission
+  // on the developer console project.
+  string description = 4;
+}
+
+// Provides links to documentation or for performing an out of band action.
+//
+// For example, if a quota check failed with an error indicating the calling
+// project hasn't enabled the accessed service, this can contain a URL pointing
+// directly to the right place in the developer console to flip the bit.
+message Help {
+  // Describes a URL link.
+  message Link {
+    // Describes what the link offers.
+    string description = 1;
+
+    // The URL of the link.
+    string url = 2;
+  }
+
+  // URL(s) pointing to additional information on handling the current error.
+  repeated Link links = 1;
+}
+
+// Provides a localized error message that is safe to return to the user
+// which can be attached to an RPC error.
+message LocalizedMessage {
+  // The locale used following the specification defined at
+  // http://www.rfc-editor.org/rfc/bcp/bcp47.txt.
+  // Examples are: "en-US", "fr-CH", "es-MX"
+  string locale = 1;
+
+  // The localized error message in the above locale.
+  string message = 2;
+}
diff --git a/rocketmq-client-csharp/Protos/google/rpc/status.proto b/rocketmq-client-csharp/Protos/google/rpc/status.proto
new file mode 100644
index 0000000..3b1f7a9
--- /dev/null
+++ b/rocketmq-client-csharp/Protos/google/rpc/status.proto
@@ -0,0 +1,47 @@
+// Copyright 2020 Google LLC
+//
+// Licensed 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.
+
+syntax = "proto3";
+
+package google.rpc;
+
+import "google/protobuf/any.proto";
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/rpc/status;status";
+option java_multiple_files = true;
+option java_outer_classname = "StatusProto";
+option java_package = "com.google.rpc";
+option objc_class_prefix = "RPC";
+
+// The `Status` type defines a logical error model that is suitable for
+// different programming environments, including REST APIs and RPC APIs. It is
+// used by [gRPC](https://github.com/grpc). Each `Status` message contains
+// three pieces of data: error code, error message, and error details.
+//
+// You can find out more about this error model and how to work with it in the
+// [API Design Guide](https://cloud.google.com/apis/design/errors).
+message Status {
+  // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
+  int32 code = 1;
+
+  // A developer-facing error message, which should be in English. Any
+  // user-facing error message should be localized and sent in the
+  // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
+  string message = 2;
+
+  // A list of messages that carry the error details.  There is a common set of
+  // message types for APIs to use.
+  repeated google.protobuf.Any details = 3;
+}
diff --git a/rocketmq-client-csharp/rocketmq-client-csharp.csproj b/rocketmq-client-csharp/rocketmq-client-csharp.csproj
new file mode 100644
index 0000000..0a1e2a6
--- /dev/null
+++ b/rocketmq-client-csharp/rocketmq-client-csharp.csproj
@@ -0,0 +1,31 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <PackageId>RocketMQ-Client-CSharp</PackageId>
+    <Version>1.0.0</Version>
+    <Authors>Zhanhui Li</Authors>
+    <Company>Apache Software Foundation</Company>
+    <TargetFramework>net5.0</TargetFramework>
+    <RootNamespace>org.apache.rocketmq</RootNamespace>
+    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Google.Protobuf" Version="3.19.4" />
+    <PackageReference Include="Grpc.Net.Client" Version="2.42.0" />
+    <PackageReference Include="Grpc.Tools" Version="2.43.0">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+
+    <Protobuf Include="Protos\apache\rocketmq\v1\definition.proto" ProtoRoot="Protos" GrpcServices="Client" />
+    <Protobuf Include="Protos\google\rpc\status.proto" ProtoRoot="Protos" GrpcServices="Client" />
+    <Protobuf Include="Protos\google\rpc\error_details.proto" ProtoRoot="Protos" GrpcServices="Client" />
+    <Protobuf Include="Protos\apache\rocketmq\v1\service.proto" ProtoRoot="Protos" GrpcServices="Client">
+      <Link>Protos\apache\rocketmq\v1\definition.proto</Link>
+      <Link>Protos\google\rpc\status.proto</Link>
+      <Link>Protos\google\rpc\error_details.proto</Link>
+    </Protobuf>
+  </ItemGroup>
+
+</Project>
diff --git a/rocketmq-client.sln b/rocketmq-client.sln
new file mode 100644
index 0000000..d3a68db
--- /dev/null
+++ b/rocketmq-client.sln
@@ -0,0 +1,62 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30114.105
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rocketmq-client-csharp", "rocketmq-client-csharp\rocketmq-client-csharp.csproj", "{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "examples", "examples\examples.csproj", "{9F749350-A3D0-423E-AFB6-79E521C777D0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tests", "tests\tests.csproj", "{92248517-D1FD-4C65-A691-647C696B9F85}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release|Any CPU = Release|Any CPU
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Debug|x64.Build.0 = Debug|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Debug|x86.Build.0 = Debug|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Release|x64.ActiveCfg = Release|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Release|x64.Build.0 = Release|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Release|x86.ActiveCfg = Release|Any CPU
+		{7BD84605-FDC5-4B47-8D5D-A9EEADA0EA99}.Release|x86.Build.0 = Release|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Debug|x64.Build.0 = Debug|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Debug|x86.Build.0 = Debug|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Release|x64.ActiveCfg = Release|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Release|x64.Build.0 = Release|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Release|x86.ActiveCfg = Release|Any CPU
+		{9F749350-A3D0-423E-AFB6-79E521C777D0}.Release|x86.Build.0 = Release|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Debug|x64.Build.0 = Debug|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Debug|x86.Build.0 = Debug|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Release|Any CPU.Build.0 = Release|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Release|x64.ActiveCfg = Release|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Release|x64.Build.0 = Release|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Release|x86.ActiveCfg = Release|Any CPU
+		{92248517-D1FD-4C65-A691-647C696B9F85}.Release|x86.Build.0 = Release|Any CPU
+	EndGlobalSection
+EndGlobal
diff --git a/tests/UnitTest1.cs b/tests/UnitTest1.cs
new file mode 100644
index 0000000..bdc4aa3
--- /dev/null
+++ b/tests/UnitTest1.cs
@@ -0,0 +1,13 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace tests
+{
+    [TestClass]
+    public class UnitTest1
+    {
+        [TestMethod]
+        public void TestMethod1()
+        {
+        }
+    }
+}
diff --git a/tests/tests.csproj b/tests/tests.csproj
new file mode 100644
index 0000000..a70533f
--- /dev/null
+++ b/tests/tests.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+    <PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
+    <PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
+    <PackageReference Include="coverlet.collector" Version="3.0.2" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\rocketmq-client-csharp\rocketmq-client-csharp.csproj" />
+  </ItemGroup>
+
+</Project>