Core Docs

Lưu (save) entity

Insert vs update, navigation property, optimistic concurrency, dirty tracking.

Save form nghe đơn giản — await client.PostAsync(entity) — nhưng trong Core có vài cạnh sắc quanh navigation property, reference, và khác biệt PATCH/POST. Đây là playbook.

Insert vs update

Cả hai đi qua cùng 1 call (base.Save(entity)). Framework quyết định:

  • entity.Id == 0insert (POST /api/Entity).
  • entity.Id > 0update (PATCH /api/Entity/{id} với 1 PatchUpdate).

EditForm.IsEditMode => Entity != null && Entity[IdField].As<int>() > 0 là cùng check, expose cho code của bạn.

Strip navigation property

Nếu entity mang collection (feature.ComponentGroup, feature.Component, feature.GridPolicy), null hoá trước khi POST trừ khi thực sự muốn nested upsert:

private async Task<bool> SaveFeatureInternal(Feature feature)
{
    feature.ClearReferences();
    feature.Component       = null;
    feature.ComponentGroup  = null;
    feature.FeaturePolicy   = null;
    feature.GridPolicy      = null;
    return await base.Save(feature);
}

Tại sao:

  • Navigation collection load qua $expand để hiển thị. POST lại bắt server reconcile — chậm trong best case, destructive trong worst case.
  • ClearReferences() (trong Core.Extensions) walk graph và null back-reference, tránh cycle khi Newtonsoft.Json serialize.

PostAsync có cờ allowNested cùng tác dụng:

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

Mặc định false strip nested. Pass true chỉ khi controller được build cho upsert children.

Patch update — chỉ field đã đổi

PatchUpdate (trong Core.ViewModels) gửi list (field, old, new) thay vì cả entity:

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

Lợi:

  • Payload nhỏ.
  • Server detect concurrent edit (so OldValue với DB hiện tại) và reject sạch.
  • Bảng History log chính xác ai đổi gì.

Phần lớn flow save form dùng PATCH tự động khi entity ở edit mode — bạn chỉ build PatchUpdate thủ công cho mutation ngoài form (toggle cờ từ row grid, …).

Dirty tracking

EditForm track dirty state qua EditableComponent.PopulateDirty. Input flip cờ khi value đổi; lúc save, framework duyệt input dirty và build PatchUpdate từ chúng. Bạn không phải track tay.

PopulateDirty = false (form admin của framework dùng) tắt dirty tracking — toàn bộ entity được save mỗi lần. Dùng khi thay đổi rải rác nhưng đi cùng một bộ.

Bulk save

Cho list entity (save nhiều row từ grid 1 lần):

var saved = await new Client(nameof(Item)).BulkUpdateAsync(items);

Server insert row Id = 0 và update phần còn lại. Trả list đã save với id mới gán.

PatchAsync<T>(List<PatchUpdate>) là bulk PATCH — dùng khi mỗi row chỉ vài field đổi.

Validation trước save

Cách Core khuyên dùng: set rule vào Component.Validation (DB) → trước submit gọi await this.IsFormValid().

public override async Task<bool> Save(object entity)
{
    // 1) Check toàn bộ rule cấu hình trong Component.Validation
    //    (Required, regex, minLength, range, custom expression, ...)
    if (!await this.IsFormValid()) return false;

    // 2) Business check riêng (unique, cross-field, server-side)
    var c = (Customer)entity;
    if (!await ValidationExtensions.IsUnique(c, nameof(Customer.Code)))
    {
        Toast.Warning("Mã đã tồn tại");
        return false;
    }

    return await base.Save(c);
}

IsFormValid() tự duyệt mọi widget visible, gọi ValidateAsync() trên từng cái → tự focus field đầu tiên sai + show toast lỗi → return false. Bạn không cần handle UI.

Đừng tự check Required / regex / minLength / max trong code C#. Set vào Component.Validation của row DB rồi để Core lo. Code C# chỉ cho business rule không cấu hình được (vd unique check từ server, cross-field validation).

AfterSaved — hook sau khi save

form.AfterSaved = ok =>
{
    if (ok) Toast.Success("Đã lưu");
    else    Toast.Warning("Lưu thất bại");
};

Cho flow phức tạp hơn — override Save thay vì hook.

Concurrency: lỗi “data đã đổi”

Server check OldValue mà có người khác sửa giữa load và save → PATCH fail. 2 lựa chọn:

  • Reload và retry. await form.LoadEntity() — show diff cho user.
  • Force overwrite. Gửi patch với OldValue = null để skip check (chỉ khi biết rõ).

Không có retry loop sẵn — design UX explicit.

Read-only / lock

EditForm.IsLock phản ánh lock server-side (vd record đang được user khác edit). Nếu true:

  • Input render disabled.
  • Save bị suppress.
  • Icon lock hiện trên header form.

Lock đến từ collection Locked trên entity load. Release lock thường khi đóng tab — Disposed handler fire Client.PostAsync để release.

Flow validation

Theo thứ tự:

  1. Cờ per-inputComponent.Validation (rule JSON), MaxLength, … render border đỏ + tooltip khi blur.
  2. await this.IsFormValid() — duyệt tất cả widget, gọi ValidateAsync() → focus field sai đầu tiên + toast lỗi.
  3. Save override — business validation thêm (unique, cross-field). Trả false để abort.
  4. Validation server — API có thể reject. Message HttpException trở thành toast.

Luôn Toast trên path abort, nếu không user thấy không có gì xảy ra.

ClearReferences làm gì?

feature.ClearReferences();

Walk graph và set back-reference (parent pointer) về null. Mục đích: tránh cycle khi Newtonsoft serialize. Sau khi gọi xong, không thể navigate ngược từ child → parent — nhưng bạn sắp send qua wire nên không cần.

Vô tình gọi rồi tiếp tục dùng entity → null deref khi navigate. Hoặc reload từ server, hoặc giữ copy riêng.

String encoded vs raw — EncodeSpecialChar

Textbox (và các input string khác) URL-encode 6 ký tự đặc biệt trước khi lưu vào entity, decode khi đọc ra. Đây KHÔNG phải HTML encode — chỉ là URL-style encoding tránh các ký tự phá OData querystring.

Bảng encode trong Utils.SpecialChar:

Ký tự gốcEncode thành
+%2B
/%2F
?%3F
#%23
&%26
'%27

Lý do: nếu value chứa &, OData query ?$filter=Code eq 'AB&CD' sẽ bị parse sai (&CD thành parameter mới).

Vd: user nhập "AB & CD" → entity lưu "AB %26 CD" → DB lưu "AB %26 CD" → đọc ra → decode lại thành "AB & CD" hiển thị.

Tắt encode: Feature.IgnoreEncode = true (hoặc Component.PlainText = true cho widget cụ thể).

Cẩn thận: bypass Textbox (set entity.Name = "AB & CD" thẳng từ JS callback) → giá trị không qua encode → khi save, OData filter có thể fail. Nhất quán: hoặc luôn-encoded (qua EncodeSpecialChar()) hoặc luôn-raw (set IgnoreEncode = true).

Tip

  • Luôn handle false từ Save. Toast lỗi, đừng silent.
  • Wrap async save trong try/catch HttpException. Network error đừng crash UI.
  • Refresh related view khi success. ShouldUpdateParentForm = true hoặc AfterSaved thủ công.
  • Đừng await user input. Build patch sync từ state hiện tại, rồi await network call.

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