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ồn | Giá trị |
|---|---|
Origin | window.OriginLocation hoặc window.Location.Origin |
Prefix | Origin + "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ìPatchAsyncvớiActive = 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
BadGatewayRequestvà retry với backoff. User thấy toast “system updating”. - 5xx khác raise
HttpException,awaitthrow. - 4xx raise
HttpExceptionvớ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. ImageUploader và FileUploadGrid dùng nội bộ.
Tránh
- Tự tạo
XHRWrappertrực tiếp. Luôn quaClientđể auth, retry, tenant routing được áp. fetchtừ JS inline. Bypass mọi thứ trongClient. Bắt buộc thì copy header auth thủ công.- Cache mọi thứ.
GetRawListcache theo URL. Data đổi server-side mà không thấy update → passclearCache: true. - Gửi nested object trong
PostAsync. Mặc địnhallowNested = falsestrip navigation property tránh payload phình và re-insert child. PassallowNested: truechỉ khi controller được build cho upsert nested.
Xem thêm
- Cách tạo 1 màn hình mới — dùng
Clientcho load + save. - GridView & ListView — grid auto issue
GetRawListdựa trênReference+DataSourceFilter.