Skip to content

Commit 9e24809

Browse files
suyask-msftclaude
andcommitted
Refine nav property language: match $metadata, not SchemaName
Use "navigation property name (case-sensitive, must match $metadata)" instead of "PascalCase SchemaName" per review feedback. System lookups like parentaccountid are lowercase, so "PascalCase" was imprecise. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8cf08b4 commit 9e24809

5 files changed

Lines changed: 18 additions & 15 deletions

File tree

.claude/skills/dataverse-sdk-dev/SKILL.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ Dataverse uses two different naming conventions for properties. Getting this wro
2626

2727
| Property type | Name convention | Example | When used |
2828
|---|---|---|---|
29-
| **Structural** (columns) | LogicalName = always lowercase | `new_name`, `new_priority` | `$select`, `$filter`, `$orderby`, record payload keys |
30-
| **Navigation** (lookups) | SchemaName = PascalCase | `new_CustomerId`, `new_AgentId` | `$expand`, `@odata.bind` annotation keys |
29+
| **Structural** (columns) | LogicalName (always lowercase) | `new_name`, `new_priority` | `$select`, `$filter`, `$orderby`, record payload keys |
30+
| **Navigation** (relationships / lookups) | Navigation Property Name (usually SchemaName, PascalCase, case-sensitive) | `new_CustomerId`, `new_AgentId` | `$expand`, `@odata.bind` annotation keys |
31+
32+
Navigation property names are case-sensitive and must match the entity's `$metadata`. Using the logical name instead of the navigation property name results in 400 Bad Request errors.
3133

3234
**Critical rule:** The OData parser validates `@odata.bind` property names **case-sensitively** against declared navigation properties. Lowercasing `new_CustomerId@odata.bind` to `new_customerid@odata.bind` causes: `ODataException: An undeclared property 'new_customerid' which only has property annotations...`
3335

.claude/skills/dataverse-sdk-use/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ for page in client.records.get(
108108
#### Create Records with Lookup Bindings (@odata.bind)
109109
```python
110110
# Set lookup fields using @odata.bind with PascalCase navigation property names
111-
# CORRECT: PascalCase SchemaName before @odata.bind
111+
# CORRECT: use the navigation property name (case-sensitive, must match $metadata)
112112
guid = client.records.create("new_ticket", {
113113
"new_name": "TKT-001",
114114
"new_CustomerId@odata.bind": f"/new_customers({customer_id})",
@@ -373,7 +373,7 @@ except ValidationError as e:
373373
- Check filter/expand parameters use correct case
374374
- Verify column names exist and are spelled correctly
375375
- Ensure custom columns include customization prefix
376-
- For `@odata.bind` errors ("undeclared property"): the navigation property name before `@odata.bind` must use **PascalCase SchemaName** (e.g., `new_CustomerId@odata.bind`), not lowercase. The OData parser is case-sensitive for navigation property names. The SDK preserves `@odata.bind` key casing and emits a warning if it detects likely-wrong lowercase casing.
376+
- For `@odata.bind` errors ("undeclared property"): the navigation property name before `@odata.bind` is case-sensitive and must match the entity's `$metadata` exactly (e.g., `new_CustomerId@odata.bind` for custom lookups, `parentaccountid@odata.bind` for system lookups). The SDK preserves `@odata.bind` key casing and emits a warning if it detects likely-wrong lowercase casing on custom lookups.
377377

378378
## Best Practices
379379

@@ -386,7 +386,7 @@ except ValidationError as e:
386386
5. **Use production credentials** - ClientSecretCredential or CertificateCredential for unattended operations
387387
6. **Error handling** - Implement retry logic for transient errors (`e.is_transient`)
388388
7. **Always include customization prefix** for custom tables/columns
389-
8. **Use lowercase for column names, PascalCase for navigation properties** - Column names in `$select`/`$filter`/record payloads use lowercase LogicalNames. Navigation properties in `$expand` and `@odata.bind` keys use PascalCase SchemaName (e.g., `new_CustomerId@odata.bind`)
389+
8. **Use lowercase for column names, match `$metadata` for navigation properties** - Column names in `$select`/`$filter`/record payloads use lowercase LogicalNames. Navigation properties in `$expand` and `@odata.bind` keys are case-sensitive and must match the entity's `$metadata` (PascalCase for custom lookups like `new_CustomerId`, lowercase for system lookups like `parentaccountid`)
390390
9. **Test in non-production environments** first
391391
10. **Use named constants** - Import cascade behavior constants from `PowerPlatform.Dataverse.common.constants`
392392

src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ for page in client.records.get(
108108
#### Create Records with Lookup Bindings (@odata.bind)
109109
```python
110110
# Set lookup fields using @odata.bind with PascalCase navigation property names
111-
# CORRECT: PascalCase SchemaName before @odata.bind
111+
# CORRECT: use the navigation property name (case-sensitive, must match $metadata)
112112
guid = client.records.create("new_ticket", {
113113
"new_name": "TKT-001",
114114
"new_CustomerId@odata.bind": f"/new_customers({customer_id})",
@@ -373,7 +373,7 @@ except ValidationError as e:
373373
- Check filter/expand parameters use correct case
374374
- Verify column names exist and are spelled correctly
375375
- Ensure custom columns include customization prefix
376-
- For `@odata.bind` errors ("undeclared property"): the navigation property name before `@odata.bind` must use **PascalCase SchemaName** (e.g., `new_CustomerId@odata.bind`), not lowercase. The OData parser is case-sensitive for navigation property names. The SDK preserves `@odata.bind` key casing and emits a warning if it detects likely-wrong lowercase casing.
376+
- For `@odata.bind` errors ("undeclared property"): the navigation property name before `@odata.bind` is case-sensitive and must match the entity's `$metadata` exactly (e.g., `new_CustomerId@odata.bind` for custom lookups, `parentaccountid@odata.bind` for system lookups). The SDK preserves `@odata.bind` key casing and emits a warning if it detects likely-wrong lowercase casing on custom lookups.
377377

378378
## Best Practices
379379

@@ -386,7 +386,7 @@ except ValidationError as e:
386386
5. **Use production credentials** - ClientSecretCredential or CertificateCredential for unattended operations
387387
6. **Error handling** - Implement retry logic for transient errors (`e.is_transient`)
388388
7. **Always include customization prefix** for custom tables/columns
389-
8. **Use lowercase for column names, PascalCase for navigation properties** - Column names in `$select`/`$filter`/record payloads use lowercase LogicalNames. Navigation properties in `$expand` and `@odata.bind` keys use PascalCase SchemaName (e.g., `new_CustomerId@odata.bind`)
389+
8. **Use lowercase for column names, match `$metadata` for navigation properties** - Column names in `$select`/`$filter`/record payloads use lowercase LogicalNames. Navigation properties in `$expand` and `@odata.bind` keys are case-sensitive and must match the entity's `$metadata` (PascalCase for custom lookups like `new_CustomerId`, lowercase for system lookups like `parentaccountid`)
390390
9. **Test in non-production environments** first
391391
10. **Use named constants** - Import cascade behavior constants from `PowerPlatform.Dataverse.common.constants`
392392

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def _lowercase_keys(record: Dict[str, Any]) -> Dict[str, Any]:
100100
101101
Keys containing ``@odata.`` (e.g. ``new_CustomerId@odata.bind``) are
102102
preserved as-is because the navigation property portion before ``@``
103-
must retain its original casing (PascalCase SchemaName). The OData
103+
must retain its original casing (case-sensitive navigation property name). The OData
104104
parser validates ``@odata.bind`` property names **case-sensitively**
105105
against the entity's declared navigation properties, so lowercasing
106106
these keys causes ``400 - undeclared property`` errors.
@@ -112,13 +112,14 @@ def _lowercase_keys(record: Dict[str, Any]) -> Dict[str, Any]:
112112
nav_prop = k.split("@odata.bind")[0]
113113
if nav_prop and nav_prop == nav_prop.lower() and "_" in nav_prop:
114114
# Likely already-lowercased navigation property name.
115-
# Navigation properties use PascalCase SchemaName (e.g.
116-
# new_CustomerId), not lowercase LogicalName.
115+
# Custom lookup navigation properties use PascalCase
116+
# (e.g. new_CustomerId), not lowercase LogicalName.
117117
warnings.warn(
118118
f"@odata.bind key '{k}' appears to use a lowercase "
119-
f"navigation property name. Dataverse requires "
120-
f"PascalCase SchemaName (e.g. 'new_CustomerId@odata.bind',"
121-
f" not 'new_customerid@odata.bind'). This will likely "
119+
f"navigation property name. Navigation property names "
120+
f"are case-sensitive and must match the entity's "
121+
f"$metadata (e.g. 'new_CustomerId@odata.bind', not "
122+
f"'new_customerid@odata.bind'). This will likely "
122123
f"cause a 400 error.",
123124
stacklevel=4,
124125
)

tests/unit/data/test_odata_internal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ def test_odata_bind_lowercase_warns(self):
371371
)
372372
odata_warnings = [x for x in w if "@odata.bind" in str(x.message)]
373373
self.assertEqual(len(odata_warnings), 1)
374-
self.assertIn("PascalCase", str(odata_warnings[0].message))
374+
self.assertIn("case-sensitive", str(odata_warnings[0].message))
375375

376376
def test_odata_bind_pascalcase_no_warning(self):
377377
"""PascalCase @odata.bind nav property does NOT emit a warning."""

0 commit comments

Comments
 (0)