Core Docs

Client (REST + OData)

Cách Core gọi API. Class Client là gateway duy nhất — GET, POST, PATCH, bulk update, hard delete.

Core.Clients.Client là gateway HTTP. Mọi call API từ FE đều đi qua nó. Build trên XHRWrapper (XMLHttpRequest thuần).

Nguồn: Core/Clients/Client.cs. Bạn đồng hành: Core/Clients/XHRWrapper.cs.

Tạo client

var c = new Client(nameof(Customer));                 // resolve /api/Customer
var c = new Client(nameof(Customer), "App.Models");   // override namespace model

Tham số đầu là tên entity, trở thành segment URL. Tham số thứ hai (tuỳ chọn) chỉ định ModelNamespace khi serialize type.

URL được ghép

{Origin}/api/{EntityName}{?subUrl}
NguồnGiá trị
Originwindow.OriginLocation hoặc window.Location.Origin
PrefixOrigin + "api"
CustomPrefix<meta name="prefix"> nếu set
Tenant<meta name="tenant">

Set CustomPrefix thay "api". Multi-tenant build dùng cái này để route theo tenant.

Đọc

Get 1 theo id

var customer = await new Client(nameof(Customer)).GetAsync<Customer>(42);

Id null/0 → trả null, không gọi server.

Get list (OData)

var rows = await new Client(nameof(Customer))
    .GetRawList<Customer>("?$filter=Active eq true and CountryId eq 1&$top=100");

Signature:

public async Task<List<T>> GetRawList<T>(
    string filter        = null,   // append vào URL — bắt đầu bằng "?"
    bool   clearCache    = false,
    bool   addTenant     = false,  // include tenant trong query string
    bool   annonymous    = false,  // bỏ auth header
    string entityName    = null    // override segment URL
);

Cache theo URL mặc định — pass clearCache: true để bust.

Get nhiều theo list id

var rows = await new Client(nameof(Customer))
    .GetRawListById<Customer>(new List<int> { 1, 2, 3 });

POST list id sang endpoint RawListById. Hữu ích khi URL $filter in quá dài.

GET URL tự do

var report = await new Client(nameof(Report))
    .GetAsync<ReportData>("CustomEndpoint?from=2026-01-01");

Khi tham số 2 là string, nó append vào URL controller.

Get first or default

var version = await new Client(nameof(Entity))
    .FirstOrDefaultAsync<Entity>("?$top=1&$filter=Name eq 'Version'");

(Pattern thực từ App.cs của TMS.UI dùng để check phiên bản.)

Ghi

POST (insert) — 1 record

var saved = await new Client(nameof(Customer)).PostAsync<Customer>(customer, "Save");

Signature:

public Task<T> PostAsync<T>(
    object value,
    string subUrl     = "",
    bool   annonymous = false,
    bool   allowNested = false,
    string finalUrl   = null);

Tham số 3 (annonymous) tắt auth — chỉ dùng cho login và POST anonymous khác.

PATCH (update) — 1 record

var ok = await new Client(nameof(Customer)).PatchAsync<bool>(new PatchUpdate
{
    Table = nameof(Customer),
    Changes = new List<PatchVM>
    {
        new() { Field = nameof(Customer.Name),  Value = "ACME" },
        new() { Field = nameof(Customer.Phone), Value = "0123" },
    },
    Key = customer.Id.ToString(),
});

PatchUpdate (trong Core.ViewModels) cho phép gửi chỉ field đã đổi, kèm ReasonOfChange và old-value để track conflict.

Bulk update

var savedRows = await new Client(nameof(ComponentGroup))
    .BulkUpdateAsync(rows);                     // mix insert/update

BulkUpdateAsync POST cả list; server insert row mới (Id == 0) và update row có sẵn. Cờ thêm: reasonOfChange, multipleThread.

Bulk patch

var saved = await new Client(nameof(Customer)).PatchAsync<Customer>(patches);

(patches = List<PatchUpdate>) — partial update nhiều record.

Hard delete

await new Client(nameof(Customer)).HardDeleteAsync(42);

// hoặc nhiều id:
await new Client(nameof(Customer)).HardDeleteAsync(new List<int> { 1, 2, 3 });

“Hard” = xoá thật, không soft-delete (Active = false). Chỉ framework cần hard delete; data app thì PatchAsync với Active = false.

Get list paging (GetList)

var page = await new Client(nameof(CoordinationDetail))
    .GetList<CoordinationDetail>($"?$filter=Active eq true and CoordinationId eq {id}");
var rows = page?.Value.ToList();

(Pattern thực từ CoordinationContainerBL.cs.) GetList trả về wrapper có Value (list rows) và metadata paging.

OData filter — cheat sheet

Server dùng OData v4 cho filter. Operator hay dùng:

$filter=Active eq true and FeatureId eq 42
$filter=startswith(Name,'AC') or contains(Code,'X')
$filter=InsertedDate gt 2026-01-01
$filter=Status/Code eq 'OPEN'             // navigate FK
$expand=Component                          // include child collection
$select=Id,Name,Code
$orderby=Name desc
$top=50&$skip=100

Ghép trong URL:

?$expand=Component&$filter=Active eq true and FeatureId eq 42&$top=200

Cần thao tác filter có sẵn (append/remove/apply clause) → dùng helper OdataExt.AppendClause / RemoveClause / ApplyClause trong Core.Extensions.OdataExt.

Authentication

Client.Token là token kiểu bearer, persist trong localStorage (UserInfo). Mọi request trừ annonymous: true đều gắn:

public static Token Token { get; set; }   // backed by LocalStorage

Server trả 401 → UnAuthorizedEventHandler fire — portal subscribe để redirect về login. User logout → gọi SignOutEventHandler?.Invoke() sau khi clear token.

Lỗi và retry

  • 502 Bad Gateway queue vào BadGatewayRequest và retry với backoff. User thấy toast “system updating”.
  • 5xx khác raise HttpException, await throw.
  • 4xx raise HttpException với body parsed.

Wrap call có thể hồi phục trong try/catch:

try
{
    var saved = await client.PostAsync<Customer>(c);
    Toast.Success("Đã lưu");
}
catch (HttpException ex)
{
    Toast.Warning(ex.Message);
}

Tenant và prefix runtime

Client.Tenant (<meta name="tenant">) đọc 1 lần và embed vào header. FileFTP trả root FTP per-tenant (<meta name="file_<TenantCode>">) hoặc default (<meta name="file">).

Muốn gọi API tenant khác trong cùng session → đổi <meta name="tenant"> rồi tạo Client mới — meta đọc lại mỗi instance.

Upload file

Helper upload sống trên Client — POST multipart/form-data lên endpoint FTP/file rồi trả URL. ImageUploaderFileUploadGrid dùng nội bộ.

Tránh

  • Tự tạo XHRWrapper trực tiếp. Luôn qua Client để auth, retry, tenant routing được áp.
  • fetch từ JS inline. Bypass mọi thứ trong Client. Bắt buộc thì copy header auth thủ công.
  • Cache mọi thứ. GetRawList cache theo URL. Data đổi server-side mà không thấy update → pass clearCache: true.
  • Gửi nested object trong PostAsync. Mặc định allowNested = false strip navigation property tránh payload phình và re-insert child. Pass allowNested: true chỉ khi controller được build cho upsert nested.

Xem thêm

Core Docs · Astro · Core.API/wwwRoot/docs