Where should custom data live in Znode 10?

Choose where custom data belongs, then read a small account mapping into a named C# model with clear missing, duplicate, and permission outcomes.

Znode 10GuideIntermediateGCG engineering guide
In this guide
  1. Entity-owned value
  2. Bounded mapping
  3. Transactional workflow
Match storage ownership and guarantees to the kind of information being maintained.

The field that became a workflow

Imagine a request to store an external service reference against an account. One field sounds simple. A month later the reference has a status, several contacts, an approval history, and two people editing it at once. The original implementation now needs rules that its storage choice never expressed.

Begin by describing the information before choosing a table. Who owns it? Is it a property of an existing Znode entity, an independent mapping, or a business transaction with its own lifecycle? Those questions tell you more than the number of fields. A single status field can carry more operational responsibility than a large descriptive record.

Three useful starting points

Use an entity attribute when the value is a property of an existing Znode entity and should follow its supported administration and API workflow. An account's external reference can fit this shape. Confirm the attribute's locale, group, and validation behavior in the target release.

Consider a Custom Table for a small, bounded lookup or mapping that does not belong to an existing entity. When the data becomes a workflow with concurrent edits, related records, transactions, or history, use project-owned relational storage with those guarantees. Keep Znode-owned entity reads and writes on the supported APIs.

Read one mapping into a named model

Place ServiceMappingRow.cs in the Custom API's NativeDataExtensions area. ServiceMappingRow names the three values your application needs: StoreId, AccountId, and ExternalReference. The SDK adapter decodes its response into these objects before ServiceMappingReader.FindSingle checks the result.

For the inspected 10.11 client, the reader uses ICustomTableDataClient.GetRecordsByTableKeyAsync with a fixed table key, a MappingKey equality filter using FilterOperators.Is, page one, and a two-row limit. Keep that version-specific response mapping in the adapter. A failed request remains an error; only a successful read with no matching rows becomes an absent mapping.

NativeDataExtensions/ServiceMappingRow.cs csharp

using System;
using System.Collections.Generic;

namespace Gcg.DeveloperExamples.Data
{
    // The SDK adapter maps its response into this project-owned model.
    public sealed class ServiceMappingRow
    {
        public int StoreId { get; set; }
        public int AccountId { get; set; }
        public string ExternalReference { get; set; }
    }

    public static class ServiceMappingReader
    {
        // Zero rows means no mapping. Transport failures remain exceptions.
        public static ServiceMappingRow FindSingle(
            IReadOnlyList<ServiceMappingRow> rows,
            int authorizedStoreId,
            int authorizedAccountId)
        {
            if (rows == null)
                throw new ArgumentNullException(nameof(rows));
            if (authorizedStoreId < 1 || authorizedAccountId < 1)
                throw new UnauthorizedAccessException("An authorized Store and account are required.");
            if (rows.Count == 0)
                return null;
            if (rows.Count != 1)
                throw new InvalidOperationException("More than one mapping matches this lookup.");

            ServiceMappingRow row = rows[0];
            if (row == null || row.StoreId < 1 || row.AccountId < 1 ||
                string.IsNullOrWhiteSpace(row.ExternalReference) || row.ExternalReference.Length > 100)
                throw new InvalidOperationException("The mapping response contains invalid fields.");
            if (row.StoreId != authorizedStoreId || row.AccountId != authorizedAccountId)
                throw new UnauthorizedAccessException("The mapping belongs to another Store or account.");

            // Return a copy so later changes to the adapter row cannot change this result.
            return new ServiceMappingRow
            {
                StoreId = row.StoreId,
                AccountId = row.AccountId,
                ExternalReference = row.ExternalReference
            };
        }
    }
}

For an authorized Store 3 and account 24, one row with those IDs and reference 'SERVICE-001' returns a copy of that ServiceMappingRow. No rows returns null. Two rows raise an integrity error. A row for account 25 raises an authorization error, even when it is the only returned row. The rest of the application reads named properties instead of knowing the SDK response's field format.

Decide what a conflicting edit means

A note edited by two operators needs a different contract from a static mapping. Give the editor a version token with its read, then require that token when saving. The update should succeed only if the stored version still matches. Otherwise return a conflict and preserve the user's proposed text for review.

Do not implement this as a separate version read followed by an unconditional update. Another writer can change the row between those operations. The comparison and update must be atomic in the chosen persistence layer. If the supported storage contract cannot provide the guarantee your workflow needs, reconsider the storage boundary rather than disguising the race in application code.

Verify the unpleasant cases first

Create fictional accounts in two stores with deliberately different mappings. Read them through authenticated contexts, then attempt a cross-account read and a mismatched stored row. Add a duplicate mapping, remove a required field, and simulate an unavailable dependency. Each should have a deliberate outcome that does not become an unrelated account's fallback.

For writable relational data, make two editors read the same version and save different values. Exactly one conditional update should succeed for that version. Verify that history and the current value remain consistent after rollback. Keep the conflicting-editor scenario in the persistence adapter's regression suite.

  • Preserve existing entity values when adding owned settings.
  • Keep credentials out of attributes and public responses.
  • Record schema and adapter versions with the release.

Write the ownership decision down

Finish with a short decision record: owner, expected query shape, write frequency, uniqueness rule, authorization scope, and recovery process. Include why the alternatives were rejected. This gives the next developer a reason to preserve the boundary instead of moving data simply because another endpoint is convenient.

Revisit the decision when the lifecycle changes. A mapping may remain a mapping for years. A record that becomes an approval queue deserves another review. The useful design is the one whose guarantees match the business behavior, including failure, rather than the one that stores the first sample fastest.

References and further reading

Bring your next engineering question.

Discuss a Znode data architecture

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.

Previous
Previous

Test configurable products and add-ons with real published fixtures

Next
Next

Show the difference between order acceptance and ERP completion