Skip to content

Adds .NET SDK port #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
node_modules
dist

dotnet/**/bin
dotnet/**/obj
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.

34 changes: 34 additions & 0 deletions AliExpressDotnetSdk.sln
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AliExpressSdk\AliExpressSdk.csproj" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions dotnet/AliExpressSdk.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> p) => Sign(p);
}

[Fact]
public void Sign_ComputesExpectedHash()
{
var client = new TestClient();
var parameters = new Dictionary<string, string>
{
["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);
}
}
9 changes: 9 additions & 0 deletions dotnet/AliExpressSdk/AliExpressSdk.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
135 changes: 135 additions & 0 deletions dotnet/AliExpressSdk/Clients/AEBaseClient.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> parameters)
{
var p = new Dictionary<string, string>(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<string, string> parameters)
{
var p = new Dictionary<string, string>(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<Result<JsonElement>> Call(IDictionary<string, string> parameters)
{
var url = Assemble(parameters);
try
{
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return new Result<JsonElement> { 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<JsonElement>
{
Ok = false,
Message = "Bad request",
ErrorResponse = error,
RequestId = error.GetProperty("request_id").GetString()
};
}
return new Result<JsonElement> { Ok = true, Data = root };
}
catch (Exception ex)
{
return new Result<JsonElement> { Ok = false, Message = ex.Message };
}
}

protected async Task<Result<JsonElement>> Execute(string method, IDictionary<string, string> parameters)
{
var p = new Dictionary<string, string>(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<Result<JsonElement>> CallApiDirectly(string method, IDictionary<string, string> parameters)
{
if (string.IsNullOrWhiteSpace(method))
{
return Task.FromResult(new Result<JsonElement> { Ok = false, Message = "Method parameter is required" });
}
var p = new Dictionary<string, string>(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);
}
}
25 changes: 25 additions & 0 deletions dotnet/AliExpressSdk/Clients/AESystemClient.cs
Original file line number Diff line number Diff line change
@@ -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<Result<JsonElement>> GenerateSecurityToken(IDictionary<string, string> args)
=> Execute("/auth/token/security/create", args);

public Task<Result<JsonElement>> GenerateToken(IDictionary<string, string> args)
=> Execute("/auth/token/create", args);

public Task<Result<JsonElement>> RefreshSecurityToken(IDictionary<string, string> args)
=> Execute("/auth/token/security/refresh", args);

public Task<Result<JsonElement>> RefreshToken(IDictionary<string, string> args)
=> Execute("/auth/token/refresh", args);
}
49 changes: 49 additions & 0 deletions dotnet/AliExpressSdk/Clients/AffiliateClient.cs
Original file line number Diff line number Diff line change
@@ -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<Result<JsonElement>> GenerateAffiliateLinks(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.link.generate", args);

public Task<Result<JsonElement>> GetCategories(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.category.get", args);

public Task<Result<JsonElement>> FeaturedPromoInfo(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.featuredpromo.get", args);

public Task<Result<JsonElement>> FeaturedPromoProducts(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.featuredpromo.products.get", args);

public Task<Result<JsonElement>> GetHotProductsDownload(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.hotproduct.download", args);

public Task<Result<JsonElement>> GetHotProducts(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.hotproduct.query", args);

public Task<Result<JsonElement>> OrderInfo(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.order.get", args);

public Task<Result<JsonElement>> OrdersList(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.order.list", args);

public Task<Result<JsonElement>> OrdersListByIndex(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.order.listbyindex", args);

public Task<Result<JsonElement>> ProductDetails(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.productdetail.get", args);

public Task<Result<JsonElement>> QueryProducts(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.product.query", args);

public Task<Result<JsonElement>> SmartMatchProducts(IDictionary<string, string> args)
=> Execute("aliexpress.affiliate.product.smartmatch", args);
}
12 changes: 12 additions & 0 deletions dotnet/AliExpressSdk/Models/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json;

namespace AliExpressSdk.Models;

public class Result<T>
{
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; }
}