Tích hợp API vận chuyển bên thứ ba để truy vấn dữ liệu logistics trong ASP.NET Core

Trong quá trình phát triển ứng dụng, việc tương tác với các hệ thống bên thứ ba thông qua API để thu thập dữ liệu là một yêu cầu phổ biến. Bài viết này sẽ hướng dẫn cách tích hợp và gọi một Open API của bên vận chuyển để truy vấn thông tin chi tiết về một vận đơn cụ thể trong môi trường ASP.NET Core.

Thông thường, các nhà cung cấp API sẽ cung cấp tài liệu chi tiết về cách thức gọi API, định dạng tham số yêu cầu và cấu trúc dữ liệu phản hồi. Nhiệm vụ của chúng ta là tuân thủ các quy tắc này để xây dựng phần mềm tương thích. Chúng ta sẽ lấy ví dụ về việc truy vấn thông tin lộ trình của một vận đơn.

1. Khởi tạo HttpClient với IHttpClientFactory

Để tạo và quản lý các đối tượng HttpClient một cách hiệu quả trong ASP.NET Core, khuyến nghị sử dụng giao diện IHttpClientFactory thay vì khởi tạo trực tiếp bằng từ khóa new. Việc sử dụng IHttpClientFactory giúp xử lý tốt hơn các vấn đề như quản lý vòng đời của HttpClient, tối ưu hóa việc tái sử dụng kết nối và tránh các sự cố về DNS caching. Trong một ứng dụng ASP.NET Core, bạn có thể inject IHttpClientFactory vào constructor và sử dụng phương thức CreateClient để tạo ra một thể hiện HttpClient được quản lý.

public class ShippingApiClient
{
    private readonly HttpClient _httpClient;

    public ShippingApiClient(IHttpClientFactory httpClientFactory)
    {
        // "ShippingTrackingClient" là một tên client đã được cấu hình trước
        _httpClient = httpClientFactory.CreateClient("ShippingTrackingClient");
    }
}

Phương thức CreateClient yêu cầu một tên định danh cho HttpClient bạn đang sử dụng, giúp phân biệt và cấu hình riêng biệt cho từng loại client.

2. Thu thập Mã thông báo (Token) Xác thực

Theo quy định của nhiều API, việc truyền một mã thông báo xác thực (token) trong các header của yêu cầu là bắt buộc để xác minh danh tính người gọi. Trước tiên, chúng ta cần định nghĩa các cấu trúc dữ liệu để gửi yêu cầu và nhận phản hồi cho việc cấp token.

A. Cấu trúc dữ liệu yêu cầu Token

public class AuthenticationRequestPayload
{
    [JsonProperty("clientId")]
    public string ClientId { get; set; }

    [JsonProperty("clientSecret")]
    public string ClientSecret { get; set; }
}

B. Cấu trúc dữ liệu phản hồi Token

public class ApiResponseBase
{
    [JsonProperty("responseCode")]
    public int ResponseCode { get; set; }

    [JsonProperty("responseMessage")]
    public string ResponseMessage { get; set; }

    [JsonProperty("isSuccessful")]
    public bool IsSuccessful { get; set; }
}

public class AuthTokenResponse : ApiResponseBase
{
    [JsonProperty("data")]
    public TokenInfo Data { get; set; }
}

public class TokenInfo
{
    [JsonProperty("refreshToken")]
    public string RefreshToken { get; set; }

    [JsonProperty("expiresInSeconds")]
    public int ExpiresInSeconds { get; set; }

    [JsonProperty("accessToken")]
    public string AccessToken { get; set; }
}

C. Phương thức gửi yêu cầu POST bất đồng bộ

Đây là một phương thức chung để gửi yêu cầu POST, trong đó nội dung yêu cầu được truyền dưới dạng JSON và các header tùy chỉnh có thể được thêm vào. Phương thức này sẽ xử lý việc tạo HttpRequestMessage và gửi nó đi.

public async Task<string> InvokeApiPostAsync(string url, string jsonBody, Dictionary<string, string> customHeaders = null)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, url);
    request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json");

    if (customHeaders != null)
    {
        foreach (var header in customHeaders)
        {
            request.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
    }

    var response = await _httpClient.SendAsync(request);
    response.EnsureSuccessStatusCode(); // Ném ngoại lệ nếu mã trạng thái không phải 2xx
    return await response.Content.ReadAsStringAsync();
}

Phương thức này sẽ gửi yêu cầu POST. Nếu yêu cầu token ban đầu thất bại hoặc token hết hạn, bạn có thể thực hiện một yêu cầu làm mới token. Cuối cùng, phản hồi JSON sẽ được giải mã thành cấu trúc AuthTokenResponse đã định nghĩa để lấy thông tin token.

3. Truy vấn dữ liệu Vận đơn

Sau khi có token xác thực, chúng ta có thể tiến hành gọi API để lấy dữ liệu nghiệp vụ. Tương tự, chúng ta cần định nghĩa các cấu trúc dữ liệu cho yêu cầu và phản hồi truy vấn vận đơn.

A. Cấu trúc dữ liệu yêu cầu truy vấn

public class ShipmentQueryRequest
{
    [JsonProperty("clientIdentifier")]
    public string ClientIdentifier { get; set; } // Mã khách hàng duy nhất

    [JsonProperty("trackingNumbers")]
    public string[] TrackingNumbers { get; set; } // Danh sách các số vận đơn cần truy vấn
}

ClientIdentifier là mã khách hàng được cung cấp bởi nhà cung cấp dịch vụ vận chuyển. TrackingNumbers là một mảng chuỗi, cho phép truy vấn thông tin của nhiều vận đơn cùng lúc.

B. Cấu trúc dữ liệu phản hồi truy vấn logistics

public class ShipmentQueryResult : ApiResponseBase
{
    [JsonProperty("data")]
    public QueryResultData Data { get; set; }

    [JsonProperty("correlationId")]
    public string CorrelationId { get; set; }
}

public class QueryResultData
{
    [JsonProperty("totalResults")]
    public string TotalResults { get; set; }

    [JsonProperty("waybillList")]
    public IList<WaybillDetails> WaybillList { get; set; }
}

public class WaybillDetails
{
    public WaybillDetails()
    {
        TrackingEvents = new List<TrackingEventDetails>();
    }

    [JsonProperty("waybillIdentifier")]
    public string WaybillIdentifier { get; set; }

    [JsonProperty("productCode")]
    public string ProductCode { get; set; }

    [JsonProperty("receiveDate")]
    public string ReceiveDate { get; set; }

    [JsonProperty("receiverName")]
    public string ReceiverName { get; set; }

    [JsonProperty("expectedDeliveryDate")]
    public string ExpectedDeliveryDate { get; set; }

    [JsonProperty("dispatchTime")]
    public string DispatchTime { get; set; }

    [JsonProperty("serviceMode")]
    public string ServiceMode { get; set; }

    [JsonProperty("originAddress")]
    public string OriginAddress { get; set; }

    [JsonProperty("destinationAddress")]
    public string DestinationAddress { get; set; }

    [JsonProperty("consigneeName")]
    public string ConsigneeName { get; set; }

    [JsonProperty("senderName")]
    public string SenderName { get; set; }

    [JsonProperty("totalFreightAmount")]
    public string TotalFreightAmount { get; set; }

    [JsonProperty("itemCount")]
    public string ItemCount { get; set; }

    [JsonProperty("freightWeight")]
    public string FreightWeight { get; set; }

    [JsonProperty("packageSize")]
    public decimal? PackageSize { get; set; }

    [JsonProperty("trackingEvents")]
    public IList<TrackingEventDetails> TrackingEvents { get; set; }
}

public class TrackingEventDetails
{
    [JsonProperty("eventId")]
    public int EventId { get; set; }

    [JsonProperty("eventStep")]
    public string EventStep { get; set; }

    [JsonProperty("eventDescription")]
    public string EventDescription { get; set; }

    [JsonProperty("eventTimestamp")]
    public string EventTimestamp { get; set; }
}

Các cấu trúc dữ liệu này cần phải khớp chính xác với định dạng được cung cấp bởi API để đảm bảo quá trình giải mã JSON diễn ra thành công.

C. Quá trình truy xuất dữ liệu nghiệp vụ

Dưới đây là phương thức chính để lấy thông tin vận đơn. Nó bao gồm các bước từ việc lấy token, kiểm tra và làm mới token, cho đến việc xây dựng yêu cầu truy vấn cuối cùng với các tham số và chữ ký điện tử.

public async Task<IList<WaybillDetails>> RetrieveWaybillTrackingAsync(ShipmentQueryRequest queryInput)
{
    var requestHeaders = new Dictionary<string, string>
    {
        { "X-api-source", "client_app" }
    };

    var authPayload = new AuthenticationRequestPayload
    {
        ClientId = ApiConstants.API_CLIENT_ID,
        ClientSecret = ApiConstants.API_CLIENT_SECRET
    };

    // 1. Lấy token ban đầu
    var tokenJson = await InvokeApiPostAsync(ApiConstants.API_TOKEN_ENDPOINT, JsonConvert.SerializeObject(authPayload), requestHeaders);
    var tokenResponse = JsonConvert.DeserializeObject<AuthTokenResponse>(tokenJson);

    // 2. Kiểm tra và làm mới token nếu cần
    if (tokenResponse?.Data?.ExpiresInSeconds <= 0 || string.IsNullOrEmpty(tokenResponse?.Data?.AccessToken))
    {
        var refreshPayload = new { refresh_token = tokenResponse?.Data?.RefreshToken };
        var refreshTokenJson = await InvokeApiPostAsync(ApiConstants.API_REFRESH_TOKEN_ENDPOINT, JsonConvert.SerializeObject(refreshPayload), requestHeaders);
        tokenResponse = JsonConvert.DeserializeObject<AuthTokenResponse>(refreshTokenJson);
    }

    if (tokenResponse?.Data == null || string.IsNullOrEmpty(tokenResponse.Data.AccessToken))
    {
        throw new InvalidOperationException("Không thể lấy hoặc làm mới mã thông báo truy cập.");
    }

    // 3. Chuẩn bị tham số cho yêu cầu truy vấn vận đơn
    var queryParameters = new Dictionary<string, string>
    {
        { "appkey", ApiConstants.API_CLIENT_ID },
        { "format", ApiConstants.API_RESPONSE_FORMAT },
        { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture) },
        { "method", ApiConstants.API_WAYBILL_QUERY_METHOD }
    };

    var jsonQueryInput = JsonConvert.SerializeObject(queryInput);
    var signature = GenerateMD5Hash(ApiConstants.API_SIGNATURE_SECRET + queryParameters["timestamp"] + jsonQueryInput).ToUpperInvariant();
    
    // Thêm chữ ký và token vào header của yêu cầu chính
    queryParameters.Add("sign", signature);
    queryParameters.Add("token", tokenResponse.Data.AccessToken);

    // 4. Gửi yêu cầu truy vấn vận đơn
    var finalResultJson = await InvokeApiPostAsync(ApiConstants.API_MAIN_API_ENDPOINT, jsonQueryInput, queryParameters);
    
    return JsonConvert.DeserializeObject<ShipmentQueryResult>(finalResultJson)?.Data?.WaybillList;
}

Một điểm quan trọng cần lưu ý là cách tính toán chữ ký điện tử (sign). Trong ví dụ này, chữ ký được tạo bằng cách mã hóa MD5 một chuỗi kết hợp giữa một khóa bí mật, thời gian hiện tại (UTC) và chuỗi JSON của các tham số nghiệp vụ. Sau đó, chữ ký này được chuyển đổi thành chữ hoa.

D. Phương thức mã hóa MD5

Đây là cách triển khai hàm mã hóa MD5 trong ASP.NET Core:

/// <summary>
/// Tạo mã hash MD5 cho một chuỗi đầu vào.
/// </summary>
/// <param name="inputString">Chuỗi cần mã hóa.</param>
/// <returns>Chuỗi hash MD5.</returns>
private string GenerateMD5Hash(string inputString)
{
    using (var md5Hasher = MD5.Create())
    {
        var hashBytes = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(inputString));
        var hexString = BitConverter.ToString(hashBytes);
        return hexString.Replace("-", "");
    }
}

Việc sử dụng khối using đảm bảo rằng tài nguyên của đối tượng MD5 sẽ được giải phóng đúng cách.

E. Các Hằng số cấu hình API

Các hằng số cần thiết cho việc gọi API có thể được định nghĩa như sau:

public static class ApiConstants
{
    public const string API_CLIENT_ID = "YOUR_APP_KEY";
    public const string API_CLIENT_SECRET = "YOUR_APP_SECRET";
    public const string API_RESPONSE_FORMAT = "json";
    public const string API_WAYBILL_QUERY_METHOD = "open.api.common.queryTracking";
    public const string API_SIGNATURE_SECRET = "YOUR_SIGN_SECRET_KEY"; // Khóa bí mật dùng để tạo chữ ký
    public const string API_TOKEN_ENDPOINT = "https://open.example.com/security/token";
    public const string API_REFRESH_TOKEN_ENDPOINT = "https://open.example.com/security/token/refresh";
    public const string API_MAIN_API_ENDPOINT = "https://open.example.com/router/api";
}

Với tất cả các thành phần trên, bạn đã có một quy trình hoàn chỉnh để tích hợp và truy xuất dữ liệu logistics từ một API bên thứ ba dựa trên số vận đơn trong ASP.NET Core.

Thẻ: ASP.NET Core HttpClientFactory REST API JSON authentication

Đăng vào ngày 7 tháng 8 lúc 13:28