If you've ever tried to set up an API template for a custom API page in Business Central and wondered why nothing seems to apply, you're not alone. The Microsoft documentation mentions that API templates only work with a specific list of standard pages, but it doesn't really explain why. That's what this post is about.
What Are API Templates?
API templates let you pre-populate fields on new records created via a POST call to a BC API. The idea is simple: when someone creates a record through the API and doesn't supply every field, the template fills in the blanks.
You set them up in API Setup
page, pick a Table/Page ID and a Template Code from your Configuration
Templates, and optionally add conditions. Done or so you'd think.
The catch is in the
docs:
API templates can only be set up with the following API pages:
contacts, countriesRegions, currencies, customers, employees, itemCategories,
paymentMethods, paymentTerms, shipmentMethods, unitsOfMeasure, and vendors.
|
API Pages
That Support Templates (Out of the Box) |
|
contacts |
|
countriesRegions |
|
currencies |
|
customers |
|
employees |
|
itemCategories |
|
paymentMethods |
|
paymentTerms |
|
shipmentMethods |
|
unitsOfMeasure |
|
vendors |
Why Only Those Pages?
Microsoft's docs don't explain
the reason, but the answer is in the code.
Every one of those supported
pages calls a specific function in the OnInsertRecord
trigger:
|
GraphMgtGeneralTools.ProcessNewRecordFromAPI(CustomerRecordRef,
TempFieldSet, CurrentDateTime()); |
This is the function that
actually looks up your API Setup configuration and applies the template values.
Without it, the framework has no hook to do anything. The template gets ignored
completely.
Here's the full OnInsertRecord from the standard Customer API page so you can see exactly how it's
wired up:
|
trigger
OnInsertRecord(BelowxRec: Boolean): Boolean |
|
var |
|
Customer: Record Customer; |
|
CustomerRecordRef: RecordRef; |
|
begin |
|
if Rec.Name = '' then |
|
Error(NotProvidedCustomerNameErr); |
|
|
|
Customer.SetRange("No.",
Rec."No."); |
|
if not Customer.IsEmpty() then |
|
Rec.Insert(); |
|
|
|
Rec.Insert(true); |
|
|
|
CustomerRecordRef.GetTable(Rec); |
|
GraphMgtGeneralTools.ProcessNewRecordFromAPI(CustomerRecordRef,
TempFieldSet, CurrentDateTime()); |
|
CustomerRecordRef.SetTable(Rec); |
|
|
|
Rec.Modify(true); |
|
SetCalculatedFields(); |
|
exit(false); |
|
end; |
The key steps are:
1. Insert the record first (so it exists in the DB)
2. Get a RecordRef from it
3. Call ProcessNewRecordFromAPI, this
is where the template lookup happens
4. Set the record back from the RecordRef
so updated values come through
5. Modify the record to save everything
Custom API pages and the
Resource API page don't have this plumbing, so templates never apply.
Seeing the Problem in Action
Let's prove this with the
Resource API page. First, create a simple Configuration Template for Resources, say, one that sets a default Base Unit of Measure to HOUR.
Go to API Setup, add a
new line, pick the Resource table or API page, and assign your template. Looks fine.
Now build Custom API Page from Scratch
|
page 50200 "My
Custom Resource API" |
|
{ |
|
PageType = API; |
|
APIPublisher = 'mohana'; |
|
APIGroup = 'ap'; |
|
APIVersion = 'v2.0'; |
|
EntityName = 'resource'; |
|
EntitySetName = 'resources'; |
|
SourceTable = Resource; |
|
ODataKeyFields = SystemId; |
|
InsertAllowed = true; |
|
DeleteAllowed = false; |
|
ModifyAllowed = true; |
|
|
|
layout |
|
{ |
|
area(Content) |
|
{ |
|
repeater(Group) |
|
{ |
|
field(id; Rec.SystemId) { } |
|
field(number;
Rec."No.") { } |
|
field(displayName; Rec.Name)
{ } |
|
field(baseUnitOfMeasure;
Rec."Base Unit of Measure") { } |
|
field(type; Rec.Type) { } |
|
} |
|
} |
|
} |
} |
Now POST a new resource via the
API without specifying Base Unit of Measure:
|
POST
/api/v2.0/companies({id})/resources |
|
{ |
|
"number":
"RES-TEST-01", |
|
"displayName": "Test
Resource" |
|
} |
Check the record that gets
created. The Base Unit of Measure field is empty. The template did
nothing.
That's because the Resource API
page's OnInsertRecord has no ProcessNewRecordFromAPI code.
The Fix: Add the Missing code
|
page 50200 "My
Custom Resource API" |
|
{ |
|
PageType = API; |
|
APIPublisher = 'mohana'; |
|
APIGroup = 'ap'; |
|
APIVersion = 'v2.0'; |
|
EntityName = 'resource'; |
|
EntitySetName = 'resources'; |
|
SourceTable = Resource; |
|
ODataKeyFields = SystemId; |
|
InsertAllowed = true; |
|
DeleteAllowed = false; |
|
ModifyAllowed = true; |
|
|
|
layout |
|
{ |
|
area(Content) |
|
{ |
|
repeater(Group) |
|
{ |
|
field(id; Rec.SystemId) { } |
|
field(number;
Rec."No.") { } |
|
field(displayName; Rec.Name)
{ } |
|
field(baseUnitOfMeasure;
Rec."Base Unit of Measure") { } |
|
field(type; Rec.Type) { } |
|
} |
|
} |
|
} |
|
|
|
var |
|
GraphMgtGeneralTools: Codeunit
"Graph Mgt - General Tools"; |
|
TempFieldSet: Record 2000000041
temporary; |
|
|
|
trigger OnInsertRecord(BelowxRec:
Boolean): Boolean |
|
var |
|
ResourceRecordRef: RecordRef; |
|
begin |
|
if Rec.Name = '' then |
|
Error('Resource display name is
required.'); |
|
|
|
Rec.Insert(true); |
|
|
|
ResourceRecordRef.GetTable(Rec); |
|
GraphMgtGeneralTools.ProcessNewRecordFromAPI( |
|
ResourceRecordRef, TempFieldSet,
CurrentDateTime()); |
|
ResourceRecordRef.SetTable(Rec); |
|
|
|
Rec.Modify(true); |
|
exit(false); |
|
end; |
|
} |
With the extension published,
run the same POST call again:
|
POST
/api/v2.0/companies({id})/resources |
|
{ |
|
"number":
"RES-TEST-02", |
|
"displayName": "Test
Resource 2" |
|
} |
This time, open the resource
record. You'll see Base Unit of Measure is populated with HOUR
from the template, even though you didn't pass it in the POST body.
The template is working exactly
as it should. The only difference is the three lines of code that hook into the
template engine.
The TempFieldSet Variable
TempFieldSet is a temporary record based on the Field
system table (table 2000000041). The ProcessNewRecordFromAPI
function uses it to track which fields were explicitly set in the API call (as
opposed to defaulted). This is how it knows which fields to leave alone and
which to fill in from the template.
BC does not populate
TempFieldSet automatically. You have
to register each field manually by calling RegisterFieldSet from
the OnValidate trigger of every field on your API page. If you skip
this, ProcessNewRecordFromAPI has no record of which fields the caller actually
sent, and it will treat all of them as update, meaning the template could
overwrite values the API caller explicitly passed in.
Here's the pattern you need on
every field:
|
field(type;
Rec."Contact Type") |
|
{ |
|
Caption = 'Type'; |
|
trigger OnValidate() |
|
begin |
|
RegisterFieldSet(Rec.FieldNo("Contact Type")); |
|
end; |
|
} |
And the RegisterFieldSet procedure itself:
|
local procedure
RegisterFieldSet(FieldNo: Integer) |
|
begin |
|
if TempFieldSet.Get(Database::Customer,
FieldNo) then |
|
exit; |
|
TempFieldSet.Init(); |
|
TempFieldSet.TableNo :=
Database::Customer; |
|
TempFieldSet.Validate("No.",
FieldNo); |
|
TempFieldSet.Insert(true); |
|
end; |
The Get
check at the top prevents duplicate entries if the same field gets validated
more than once. Every field you expose on the API page needs its own OnValidate
trigger calling this procedure. If you miss a field, the template will
overwrite whatever the caller sent for that field.
What If You Pass a Value That's Also in the Template?
The API caller always wins. ProcessNewRecordFromAPI only applies a template value if that field is not
already in TempFieldSet. Since every field you explicitly send in the POST
body gets tracked there, the function simply skips those fields when applying
the template.
So if your template sets Type
to Machine but your POST includes "type": "Person", the record gets Machine without TempFieldSet.
If your template sets Type to Machine but your POST includes "type": "Person", the record gets Person with TempFieldSet.
The template value is ignored
for that field.
This matches what the Microsoft docs say about template ordering too:
Field values defined in the template are only applied to fields
that have not already had a value defined, either explicitly in the API, or in
a previously applied template in the order.
The same rule applies when you
have multiple templates stacked in API Setup with different order numbers. The
first template to set a field wins, and any later templates skip it.
|
Priority |
Source |
Wins over |
|
1 (highest) |
Explicit API value |
Everything |
|
2 |
Earlier template (lower
Order #) |
Later templates |
|
3 (lowest) |
Later template (higher
Order #) |
Nothing |
No comments:
Post a Comment