From e1d76b8798a82f592cce1cfc324e65c9224e740a Mon Sep 17 00:00:00 2001
From: Kusp00110110 <113149818+Kusp00110110@users.noreply.github.com>
Date: Tue, 19 Aug 2025 12:18:52 +0200
Subject: [PATCH 1/3] chore: clean build outputs
---
.gitignore | 3 +
AGENTS.md | 9 ++
AliExpressDotnetSdk.sln | 34 +++++
.../AliExpressSdk.Tests.csproj | 27 ++++
dotnet/AliExpressSdk.Tests/UnitTest1.cs | 31 ++++
dotnet/AliExpressSdk/AliExpressSdk.csproj | 9 ++
dotnet/AliExpressSdk/Clients/AEBaseClient.cs | 135 ++++++++++++++++++
.../AliExpressSdk/Clients/AESystemClient.cs | 25 ++++
.../AliExpressSdk/Clients/AffiliateClient.cs | 49 +++++++
dotnet/AliExpressSdk/Models/Result.cs | 12 ++
10 files changed, 334 insertions(+)
create mode 100644 AGENTS.md
create mode 100644 AliExpressDotnetSdk.sln
create mode 100644 dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj
create mode 100644 dotnet/AliExpressSdk.Tests/UnitTest1.cs
create mode 100644 dotnet/AliExpressSdk/AliExpressSdk.csproj
create mode 100644 dotnet/AliExpressSdk/Clients/AEBaseClient.cs
create mode 100644 dotnet/AliExpressSdk/Clients/AESystemClient.cs
create mode 100644 dotnet/AliExpressSdk/Clients/AffiliateClient.cs
create mode 100644 dotnet/AliExpressSdk/Models/Result.cs
diff --git a/.gitignore b/.gitignore
index f06235c..bdb46ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
node_modules
dist
+
+dotnet/**/bin
+dotnet/**/obj
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..3971fc7
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,9 @@
+# Repository Purpose
+
+This repository contains a TypeScript implementation of the AliExpress SDK and its .NET port. It serves as a blueprint and reference for building AliExpress integrations in different ecosystems.
+
+# Development Guidelines
+
+- Ensure that every change builds and tests the .NET solution.
+- Run `dotnet build` and `dotnet test` before committing.
+
diff --git a/AliExpressDotnetSdk.sln b/AliExpressDotnetSdk.sln
new file mode 100644
index 0000000..4693177
--- /dev/null
+++ b/AliExpressDotnetSdk.sln
@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet", "dotnet", "{1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk", "dotnet\AliExpressSdk\AliExpressSdk.csproj", "{BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk.Tests", "dotnet\AliExpressSdk.Tests\AliExpressSdk.Tests.csproj", "{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224}
+ {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224}
+ EndGlobalSection
+EndGlobal
diff --git a/dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj b/dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj
new file mode 100644
index 0000000..789b27a
--- /dev/null
+++ b/dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/AliExpressSdk.Tests/UnitTest1.cs b/dotnet/AliExpressSdk.Tests/UnitTest1.cs
new file mode 100644
index 0000000..98ec673
--- /dev/null
+++ b/dotnet/AliExpressSdk.Tests/UnitTest1.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using AliExpressSdk.Clients;
+using Xunit;
+
+namespace AliExpressSdk.Tests;
+
+public class SigningTests
+{
+ private class TestClient : AEBaseClient
+ {
+ public TestClient() : base("test", "secret", "session") { }
+ public string DoSign(IDictionary p) => Sign(p);
+ }
+
+ [Fact]
+ public void Sign_ComputesExpectedHash()
+ {
+ var client = new TestClient();
+ var parameters = new Dictionary
+ {
+ ["method"] = "/auth/token/create",
+ ["app_key"] = "test",
+ ["session"] = "session",
+ ["timestamp"] = "12345",
+ ["simplify"] = "true",
+ ["sign_method"] = "sha256"
+ };
+ var sign = client.DoSign(parameters);
+ Assert.Equal("083482F9A0CE8559B567E46222AA1401B09BBDACC409D0BDA77A9A385A0BD31C", sign);
+ }
+}
diff --git a/dotnet/AliExpressSdk/AliExpressSdk.csproj b/dotnet/AliExpressSdk/AliExpressSdk.csproj
new file mode 100644
index 0000000..bb23fb7
--- /dev/null
+++ b/dotnet/AliExpressSdk/AliExpressSdk.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/dotnet/AliExpressSdk/Clients/AEBaseClient.cs b/dotnet/AliExpressSdk/Clients/AEBaseClient.cs
new file mode 100644
index 0000000..3580a13
--- /dev/null
+++ b/dotnet/AliExpressSdk/Clients/AEBaseClient.cs
@@ -0,0 +1,135 @@
+using System.Linq;
+using System.Globalization;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using AliExpressSdk.Models;
+
+namespace AliExpressSdk.Clients;
+
+public class AEBaseClient
+{
+ private readonly HttpClient _httpClient;
+ public string AppKey { get; }
+ public string AppSecret { get; }
+ public string Session { get; }
+
+ private const string TopApiUrl = "https://api-sg.aliexpress.com/sync";
+ private const string OpApiUrl = "https://api-sg.aliexpress.com/rest";
+ private const string SignMethod = "sha256";
+
+ public AEBaseClient(string appKey, string appSecret, string session, HttpClient? httpClient = null)
+ {
+ AppKey = appKey;
+ AppSecret = appSecret;
+ Session = session;
+ _httpClient = httpClient ?? new HttpClient();
+ }
+
+ protected string Sign(IDictionary parameters)
+ {
+ var p = new Dictionary(parameters);
+ var baseString = string.Empty;
+ if (p.TryGetValue("method", out var method) && method.Contains('/'))
+ {
+ baseString = method;
+ p.Remove("method");
+ }
+
+ foreach (var kv in p.Where(kv => kv.Value != null).OrderBy(kv => kv.Key))
+ {
+ baseString += kv.Key + kv.Value;
+ }
+
+ using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(AppSecret));
+ var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(baseString));
+ return string.Concat(hash.Select(b => b.ToString("X2", CultureInfo.InvariantCulture)));
+ }
+
+ protected string Assemble(IDictionary parameters)
+ {
+ var p = new Dictionary(parameters);
+ var baseUrl = p["method"].Contains('/') ? $"{OpApiUrl}{p["method"]}" : TopApiUrl;
+ if (p["method"].Contains('/'))
+ {
+ p.Remove("method");
+ }
+
+ var query = string.Join("&", p
+ .Where(kv => kv.Value != null)
+ .OrderBy(kv => kv.Key)
+ .Select((kv, idx) =>
+ {
+ var prefix = idx == 0 ? "?" : "&";
+ return prefix + Uri.EscapeDataString(kv.Key) + "=" + Uri.EscapeDataString(kv.Value);
+ }));
+
+ return baseUrl + query;
+ }
+
+ protected async Task> Call(IDictionary parameters)
+ {
+ var url = Assemble(parameters);
+ try
+ {
+ var response = await _httpClient.GetAsync(url);
+ if (!response.IsSuccessStatusCode)
+ {
+ return new Result { Ok = false, Message = $"HTTP Error: {(int)response.StatusCode} {response.ReasonPhrase}" };
+ }
+ var content = await response.Content.ReadAsStringAsync();
+ var doc = JsonDocument.Parse(content);
+ var root = doc.RootElement;
+ if (root.TryGetProperty("error_response", out var error))
+ {
+ return new Result
+ {
+ Ok = false,
+ Message = "Bad request",
+ ErrorResponse = error,
+ RequestId = error.GetProperty("request_id").GetString()
+ };
+ }
+ return new Result { Ok = true, Data = root };
+ }
+ catch (Exception ex)
+ {
+ return new Result { Ok = false, Message = ex.Message };
+ }
+ }
+
+ protected async Task> Execute(string method, IDictionary parameters)
+ {
+ var p = new Dictionary(parameters)
+ {
+ ["method"] = method,
+ ["session"] = Session,
+ ["app_key"] = AppKey,
+ ["simplify"] = "true",
+ ["sign_method"] = SignMethod,
+ ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture)
+ };
+ p["sign"] = Sign(p);
+ return await Call(p);
+ }
+
+ public Task> CallApiDirectly(string method, IDictionary parameters)
+ {
+ if (string.IsNullOrWhiteSpace(method))
+ {
+ return Task.FromResult(new Result { Ok = false, Message = "Method parameter is required" });
+ }
+ var p = new Dictionary(parameters)
+ {
+ ["method"] = method,
+ ["session"] = Session,
+ ["app_key"] = AppKey,
+ ["simplify"] = "true",
+ ["sign_method"] = SignMethod,
+ ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture)
+ };
+ p["sign"] = Sign(p);
+ return Call(p);
+ }
+}
diff --git a/dotnet/AliExpressSdk/Clients/AESystemClient.cs b/dotnet/AliExpressSdk/Clients/AESystemClient.cs
new file mode 100644
index 0000000..5565eda
--- /dev/null
+++ b/dotnet/AliExpressSdk/Clients/AESystemClient.cs
@@ -0,0 +1,25 @@
+using System.Net.Http;
+using System.Text.Json;
+using AliExpressSdk.Models;
+
+namespace AliExpressSdk.Clients;
+
+public class AESystemClient : AEBaseClient
+{
+ public AESystemClient(string appKey, string appSecret, string session, HttpClient? httpClient = null)
+ : base(appKey, appSecret, session, httpClient)
+ {
+ }
+
+ public Task> GenerateSecurityToken(IDictionary args)
+ => Execute("/auth/token/security/create", args);
+
+ public Task> GenerateToken(IDictionary args)
+ => Execute("/auth/token/create", args);
+
+ public Task> RefreshSecurityToken(IDictionary args)
+ => Execute("/auth/token/security/refresh", args);
+
+ public Task> RefreshToken(IDictionary args)
+ => Execute("/auth/token/refresh", args);
+}
diff --git a/dotnet/AliExpressSdk/Clients/AffiliateClient.cs b/dotnet/AliExpressSdk/Clients/AffiliateClient.cs
new file mode 100644
index 0000000..9ddb6c8
--- /dev/null
+++ b/dotnet/AliExpressSdk/Clients/AffiliateClient.cs
@@ -0,0 +1,49 @@
+using System.Net.Http;
+using System.Text.Json;
+using AliExpressSdk.Models;
+
+namespace AliExpressSdk.Clients;
+
+public class AffiliateClient : AESystemClient
+{
+ public AffiliateClient(string appKey, string appSecret, string session, HttpClient? httpClient = null)
+ : base(appKey, appSecret, session, httpClient)
+ {
+ }
+
+ public Task> GenerateAffiliateLinks(IDictionary args)
+ => Execute("aliexpress.affiliate.link.generate", args);
+
+ public Task> GetCategories(IDictionary args)
+ => Execute("aliexpress.affiliate.category.get", args);
+
+ public Task> FeaturedPromoInfo(IDictionary args)
+ => Execute("aliexpress.affiliate.featuredpromo.get", args);
+
+ public Task> FeaturedPromoProducts(IDictionary args)
+ => Execute("aliexpress.affiliate.featuredpromo.products.get", args);
+
+ public Task> GetHotProductsDownload(IDictionary args)
+ => Execute("aliexpress.affiliate.hotproduct.download", args);
+
+ public Task> GetHotProducts(IDictionary args)
+ => Execute("aliexpress.affiliate.hotproduct.query", args);
+
+ public Task> OrderInfo(IDictionary args)
+ => Execute("aliexpress.affiliate.order.get", args);
+
+ public Task> OrdersList(IDictionary args)
+ => Execute("aliexpress.affiliate.order.list", args);
+
+ public Task> OrdersListByIndex(IDictionary args)
+ => Execute("aliexpress.affiliate.order.listbyindex", args);
+
+ public Task> ProductDetails(IDictionary args)
+ => Execute("aliexpress.affiliate.productdetail.get", args);
+
+ public Task> QueryProducts(IDictionary args)
+ => Execute("aliexpress.affiliate.product.query", args);
+
+ public Task> SmartMatchProducts(IDictionary args)
+ => Execute("aliexpress.affiliate.product.smartmatch", args);
+}
diff --git a/dotnet/AliExpressSdk/Models/Result.cs b/dotnet/AliExpressSdk/Models/Result.cs
new file mode 100644
index 0000000..5e8536d
--- /dev/null
+++ b/dotnet/AliExpressSdk/Models/Result.cs
@@ -0,0 +1,12 @@
+using System.Text.Json;
+
+namespace AliExpressSdk.Models;
+
+public class Result
+{
+ public bool Ok { get; set; }
+ public string? Message { get; set; }
+ public T? Data { get; set; }
+ public JsonElement? ErrorResponse { get; set; }
+ public string? RequestId { get; set; }
+}
From c34e6b0473cfe4dfe5c3ce5764110ea9b4bad4ec Mon Sep 17 00:00:00 2001
From: Kusp00110110 <113149818+Kusp00110110@users.noreply.github.com>
Date: Fri, 24 Oct 2025 20:51:14 +0200
Subject: [PATCH 2/3] Add NopCommerce plugin planning checklist
---
AGENTS.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
index 3971fc7..7976ae3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,3 +7,15 @@ This repository contains a TypeScript implementation of the AliExpress SDK and i
- Ensure that every change builds and tests the .NET solution.
- Run `dotnet build` and `dotnet test` before committing.
+## NopCommerce Plugin Implementation Plan
+- [ ] Review the provided documentation on writing NopCommerce plugins and the Visual Studio template structure to capture required conventions and extension points.
+- [ ] Define the plugin's high-level architecture, including catalog synchronization flow, AliExpress API integration touchpoints, configuration settings, and background tasks.
+- [ ] Scaffold a new plugin project in a dedicated root-level folder using the official template as a baseline, adapting namespaces and project metadata for AliExpress drop shipping.
+- [ ] Integrate the existing AliExpress SDK into the plugin solution, establishing service abstractions for product search, catalog import, and order fulfillment.
+- [ ] Implement catalog import features, including UI components for searching AliExpress products, mapping product data, and persisting selections into the NopCommerce catalog.
+- [ ] Implement drop-shipping order automation, handling order placement to AliExpress, tracking statuses, and synchronizing fulfillment updates back to NopCommerce.
+- [ ] Add administrative configuration pages, localization resources, and permission management required for managing the plugin within NopCommerce.
+- [ ] Create automated tests, sample configuration instructions, and deployment documentation to validate and support the plugin.
+
+Next item to action: Review the documentation resources above and mark the first step as completed once finished.
+
From 66bff0b71101b6b43b1e64f7f9e8e6a16fd2a042 Mon Sep 17 00:00:00 2001
From: Kusp00110110 <113149818+Kusp00110110@users.noreply.github.com>
Date: Mon, 3 Nov 2025 17:32:51 +0200
Subject: [PATCH 3/3] Add .NET console harness and Azure DevOps pipelines
---
AGENTS.md | 1 +
AliExpressDotnetSdk.sln | 7 +
README.md | 46 ++++-
azure-pipelines-coverage.yml | 39 ++++
azure-pipelines.yml | 47 +++++
.../AliExpressSdk.ConsoleHarness.csproj | 14 ++
.../AliExpressSdk.ConsoleHarness/Program.cs | 178 ++++++++++++++++++
7 files changed, 330 insertions(+), 2 deletions(-)
create mode 100644 azure-pipelines-coverage.yml
create mode 100644 azure-pipelines.yml
create mode 100644 dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj
create mode 100644 dotnet/AliExpressSdk.ConsoleHarness/Program.cs
diff --git a/AGENTS.md b/AGENTS.md
index 7976ae3..e95d7c6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,6 +6,7 @@ This repository contains a TypeScript implementation of the AliExpress SDK and i
- Ensure that every change builds and tests the .NET solution.
- Run `dotnet build` and `dotnet test` before committing.
+- After making changes, add or update unit tests to maintain and improve coverage over time.
## NopCommerce Plugin Implementation Plan
- [ ] Review the provided documentation on writing NopCommerce plugins and the Visual Studio template structure to capture required conventions and extension points.
diff --git a/AliExpressDotnetSdk.sln b/AliExpressDotnetSdk.sln
index 4693177..f3f7938 100644
--- a/AliExpressDotnetSdk.sln
+++ b/AliExpressDotnetSdk.sln
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk", "dotnet\Ali
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk.Tests", "dotnet\AliExpressSdk.Tests\AliExpressSdk.Tests.csproj", "{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk.ConsoleHarness", "dotnet\AliExpressSdk.ConsoleHarness\AliExpressSdk.ConsoleHarness.csproj", "{DB6E8D4D-D753-435F-9B6D-9783966C9F3C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -26,9 +28,14 @@ Global
{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224}
{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224}
+ {DB6E8D4D-D753-435F-9B6D-9783966C9F3C} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224}
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 817486c..d15ab8b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# AliExpress SDK 
+# AliExpress SDK (.NET Port) 


@@ -9,11 +9,24 @@
A simple, lightweight, and fully type-safe SDK for the AliExpress Open Platform APIs. Supports System Authentication, Dropshipping, and Affiliate APIs.
+This repository contains the original TypeScript implementation published to npm **and** a .NET 8 port that mirrors the same API surface for C# developers. Both implementations share the same intent: provide a pragmatic, well-tested toolkit for working with the AliExpress Open Platform.
+
+## 🧱 Project Structure
+
+| Folder | Description |
+| ------ | ----------- |
+| `src/` | TypeScript source for the npm package. |
+| `dotnet/AliExpressSdk` | Core .NET SDK that ports the TypeScript features to C#. |
+| `dotnet/AliExpressSdk.Tests` | xUnit test suite with Coverlet coverage collection. |
+| `dotnet/AliExpressSdk.ConsoleHarness` | Console-based harness for manual and integration testing against live AliExpress APIs. |
+
+The solution file `AliExpressDotnetSdk.sln` ties the .NET projects together for local development, automated tests, and packaging.
+
## 📖 Overview
AliExpress has completely migrated their services from the legacy [Taobao Open Platform](https://developers.aliexpress.com) to the new [Open Platform for International Developers](https://openservice.aliexpress.com). While this update brought significant improvements, [they have yet to release an official Node.js SDK](https://openservice.aliexpress.com/doc/doc.htm?nodeId=27493&docId=118729#/?docId=1371) for the new platform.
-This unofficial SDK bridges that gap by providing a simple, consistent interface for Node.js developers to interact with AliExpress APIs.
+This unofficial SDK bridges that gap by providing a simple, consistent interface for Node.js developers **and** a faithful .NET port so that teams can choose whichever stack best suits their integration requirements.
## ✨ Features
@@ -138,6 +151,35 @@ All API methods return a consistent response structure:
}
```
+## 🧪 .NET Integration Harness
+
+Use the console harness to exercise live API calls without having to write a bespoke application. The harness reads credentials from environment variables and accepts request parameters via command-line arguments or a JSON payload file.
+
+```bash
+export AE_APP_KEY="your_app_key"
+export AE_APP_SECRET="your_app_secret"
+export AE_SESSION="active_session_token"
+
+dotnet run --project dotnet/AliExpressSdk.ConsoleHarness /auth/token/refresh refresh_token=xxxx
+# or supply a payload file
+dotnet run --project dotnet/AliExpressSdk.ConsoleHarness /auth/token/security/create --payload payload.json
+```
+
+Arguments passed as `key=value` are converted into request parameters. When `--payload` is supplied, the harness expects a JSON object whose properties are merged into the request.
+
+## 🛠️ Build, Test, and Packaging
+
+- `dotnet build AliExpressDotnetSdk.sln` builds the .NET port.
+- `dotnet test AliExpressDotnetSdk.sln --collect "XPlat Code Coverage"` executes the xUnit suite and collects coverage.
+- `dotnet pack dotnet/AliExpressSdk/AliExpressSdk.csproj -c Release` produces a NuGet package locally.
+
+For automated builds, the repository includes Azure DevOps pipeline definitions:
+
+| File | Purpose |
+| ---- | ------- |
+| `azure-pipelines.yml` | Restores, builds, tests, and packs the SDK, then publishes the NuGet package to the build artifact staging directory. |
+| `azure-pipelines-coverage.yml` | Runs the test suite with default coverage settings and publishes both the test results and Cobertura coverage report. |
+
## 📚 API Examples
### System Client
diff --git a/azure-pipelines-coverage.yml b/azure-pipelines-coverage.yml
new file mode 100644
index 0000000..93c2daa
--- /dev/null
+++ b/azure-pipelines-coverage.yml
@@ -0,0 +1,39 @@
+trigger: none
+
+pool:
+ vmImage: 'windows-latest'
+
+variables:
+ buildConfiguration: 'Debug'
+ dotnetSdkVersion: '8.0.x'
+ solution: 'AliExpressDotnetSdk.sln'
+
+steps:
+- task: UseDotNet@2
+ displayName: 'Install .NET SDK'
+ inputs:
+ packageType: 'sdk'
+ version: '$(dotnetSdkVersion)'
+
+- script: dotnet restore $(solution)
+ displayName: 'Restore solution'
+
+- script: dotnet test $(solution) --configuration $(buildConfiguration) --logger "trx;LogFileName=TestResults.trx" --collect "XPlat Code Coverage"
+ displayName: 'Run tests with coverage'
+
+- task: PublishTestResults@2
+ displayName: 'Publish test results'
+ inputs:
+ testResultsFormat: 'VSTest'
+ testResultsFiles: '**/TestResults.trx'
+ searchFolder: '$(System.DefaultWorkingDirectory)'
+ testRunTitle: 'Coverage Tests'
+ failTaskOnFailedTests: true
+
+- task: PublishCodeCoverageResults@1
+ displayName: 'Publish coverage report'
+ inputs:
+ codeCoverageTool: 'Cobertura'
+ summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.cobertura.xml'
+ reportDirectory: '$(System.DefaultWorkingDirectory)'
+ failIfCoverageEmpty: true
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000..d35228d
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,47 @@
+trigger:
+- main
+
+pool:
+ vmImage: 'windows-latest'
+
+variables:
+ buildConfiguration: 'Release'
+ dotnetSdkVersion: '8.0.x'
+ packageProject: 'dotnet/AliExpressSdk/AliExpressSdk.csproj'
+ solution: 'AliExpressDotnetSdk.sln'
+ artifactName: 'nuget-package'
+
+steps:
+- task: UseDotNet@2
+ displayName: 'Install .NET SDK'
+ inputs:
+ packageType: 'sdk'
+ version: '$(dotnetSdkVersion)'
+
+- script: dotnet restore $(solution)
+ displayName: 'Restore solution'
+
+- script: dotnet build $(solution) --configuration $(buildConfiguration) --no-restore
+ displayName: 'Build solution'
+
+- script: dotnet test $(solution) --configuration $(buildConfiguration) --no-build --logger "trx;LogFileName=TestResults.trx"
+ displayName: 'Run tests'
+
+- task: PublishTestResults@2
+ displayName: 'Publish test results'
+ inputs:
+ testResultsFormat: 'VSTest'
+ testResultsFiles: '**/TestResults.trx'
+ searchFolder: '$(System.DefaultWorkingDirectory)'
+ testRunTitle: 'Unit Tests'
+ failTaskOnFailedTests: true
+
+- script: dotnet pack $(packageProject) --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)/nuget
+ displayName: 'Pack NuGet package'
+
+- task: PublishBuildArtifacts@1
+ displayName: 'Publish NuGet package artifact'
+ inputs:
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)/nuget'
+ ArtifactName: '$(artifactName)'
+ publishLocation: 'Container'
diff --git a/dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj b/dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj
new file mode 100644
index 0000000..a8ebd3e
--- /dev/null
+++ b/dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/dotnet/AliExpressSdk.ConsoleHarness/Program.cs b/dotnet/AliExpressSdk.ConsoleHarness/Program.cs
new file mode 100644
index 0000000..424c2a1
--- /dev/null
+++ b/dotnet/AliExpressSdk.ConsoleHarness/Program.cs
@@ -0,0 +1,178 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using AliExpressSdk.Clients;
+
+if (args.Length == 0)
+{
+ PrintUsage();
+ return;
+}
+
+var configuration = ReadConfiguration();
+if (configuration == null)
+{
+ Console.Error.WriteLine("Required environment variables AE_APP_KEY, AE_APP_SECRET, or AE_SESSION are missing.");
+ Console.Error.WriteLine("Set them before running the harness.");
+ Environment.ExitCode = 1;
+ return;
+}
+
+var (method, remainingArgs) = ParseMethod(args);
+if (string.IsNullOrWhiteSpace(method))
+{
+ Console.Error.WriteLine("No API method supplied.");
+ PrintUsage();
+ Environment.ExitCode = 1;
+ return;
+}
+
+var payloadFile = GetOptionValue("--payload", ref remainingArgs);
+Dictionary parameters;
+try
+{
+ parameters = BuildParameters(remainingArgs, payloadFile);
+}
+catch (Exception ex)
+{
+ Console.Error.WriteLine($"Failed to read parameters: {ex.Message}");
+ Environment.ExitCode = 1;
+ return;
+}
+
+var client = new AEBaseClient(configuration.Value.AppKey, configuration.Value.AppSecret, configuration.Value.Session);
+var result = await client.CallApiDirectly(method!, parameters);
+
+if (result.Ok && result.Data is { } data)
+{
+ Console.WriteLine("Request succeeded.");
+ Console.WriteLine(JsonSerializer.Serialize(data, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ }));
+}
+else
+{
+ Console.Error.WriteLine("Request failed.");
+ if (!string.IsNullOrWhiteSpace(result.Message))
+ {
+ Console.Error.WriteLine($"Message: {result.Message}");
+ }
+
+ if (result.ErrorResponse is { } error)
+ {
+ Console.Error.WriteLine("AliExpress error response:");
+ Console.Error.WriteLine(JsonSerializer.Serialize(error, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ }));
+ }
+}
+
+static (string? Method, string[] RemainingArgs) ParseMethod(string[] args)
+{
+ if (args.Length == 0)
+ {
+ return (null, Array.Empty());
+ }
+
+ return (args[0], args.Skip(1).ToArray());
+}
+
+static string? GetOptionValue(string optionName, ref string[] args)
+{
+ var remaining = new List();
+ string? value = null;
+
+ for (var i = 0; i < args.Length; i++)
+ {
+ if (args[i] == optionName)
+ {
+ if (i + 1 >= args.Length)
+ {
+ throw new ArgumentException($"The option {optionName} requires a value.");
+ }
+
+ value = args[i + 1];
+ i++; // Skip value
+ }
+ else
+ {
+ remaining.Add(args[i]);
+ }
+ }
+
+ args = remaining.ToArray();
+ return value;
+}
+
+static Dictionary BuildParameters(IEnumerable args, string? payloadFile)
+{
+ var parameters = new Dictionary(StringComparer.Ordinal);
+
+ if (!string.IsNullOrWhiteSpace(payloadFile))
+ {
+ if (!File.Exists(payloadFile))
+ {
+ throw new FileNotFoundException("Payload file not found.", payloadFile);
+ }
+
+ using var payloadStream = File.OpenRead(payloadFile);
+ var payload = JsonSerializer.Deserialize>(payloadStream);
+ if (payload != null)
+ {
+ foreach (var kvp in payload)
+ {
+ parameters[kvp.Key] = kvp.Value;
+ }
+ }
+ }
+
+ foreach (var argument in args)
+ {
+ var separatorIndex = argument.IndexOf('=');
+ if (separatorIndex < 1 || separatorIndex == argument.Length - 1)
+ {
+ throw new ArgumentException($"Could not parse parameter '{argument}'. Use the format key=value.");
+ }
+
+ var key = argument[..separatorIndex];
+ var value = argument[(separatorIndex + 1)..];
+ parameters[key] = value;
+ }
+
+ return parameters;
+}
+
+static (string AppKey, string AppSecret, string Session)? ReadConfiguration()
+{
+ var appKey = Environment.GetEnvironmentVariable("AE_APP_KEY");
+ var appSecret = Environment.GetEnvironmentVariable("AE_APP_SECRET");
+ var session = Environment.GetEnvironmentVariable("AE_SESSION");
+
+ if (string.IsNullOrWhiteSpace(appKey) || string.IsNullOrWhiteSpace(appSecret) || string.IsNullOrWhiteSpace(session))
+ {
+ return null;
+ }
+
+ return (appKey, appSecret, session);
+}
+
+static void PrintUsage()
+{
+ Console.WriteLine("AliExpress SDK Console Harness");
+ Console.WriteLine();
+ Console.WriteLine("Usage:");
+ Console.WriteLine(" dotnet run --project dotnet/AliExpressSdk.ConsoleHarness [key=value ...] [--payload payload.json]");
+ Console.WriteLine();
+ Console.WriteLine("Environment variables:");
+ Console.WriteLine(" AE_APP_KEY - Your AliExpress app key.");
+ Console.WriteLine(" AE_APP_SECRET - Your AliExpress app secret.");
+ Console.WriteLine(" AE_SESSION - Active session token.");
+ Console.WriteLine();
+ Console.WriteLine("Arguments:");
+ Console.WriteLine(" method - API method path (e.g. /auth/token/create or aliexpress.solution.something).");
+ Console.WriteLine(" key=value - Additional request parameters.");
+ Console.WriteLine(" --payload - Optional path to JSON file with additional parameters.");
+}