Znode 9 to 10: migrate behavior before translating classes
Turn legacy customization code into explicit behaviors, then rebuild and verify one complete journey across the target platform.
In this guide
A successful build can still be a failed migration
Suppose a custom account note compiles on the new platform. The screen opens and the save button returns success. Yet the new endpoint permits another account's note to be edited, loses a simultaneous update, and returns timestamps in a different format. The class was translated, but the behavior was not preserved.
Treat source code as evidence of the existing workflow, not as the migration specification. The specification must include who may act, which business entity owns the data, what the user sees, what gets persisted, and what happens when a dependency fails. Those details often cross several legacy classes and configuration files.
Inventory a journey rather than a folder
Choose a concrete journey such as viewing and updating an account note. Trace it from browser interaction through controller, application service, data access, and response rendering. Record any session state, cache entries, integration calls, feature settings, and background work involved. A dependency that is invisible in the happy path can still determine the release order.
Separate business rules from platform mechanics. 'An authorized buyer can update the selected account's note' is a rule. A particular controller base class is a mechanism. Preserve or intentionally revise the rule, then choose a supported mechanism on the target platform. This prevents legacy hosting assumptions from becoming accidental architecture requirements.
Compare a complete behavior contract
Add AccountNoteContract.cs to the project-owned contract layer and map both platform responses into AccountNote. RequireEquivalent compares the observable account and text behavior. ApplyEdit validates ownership and a revision before creating the next immutable value.
The revision guard belongs inside the persistence adapter's atomic conditional write as well. Use this function to exercise rules independently, then invoke it from the target Custom API service. Znode's API customization guide establishes that extension boundary; keep Znode-owned reads behind the supplied clients.
Contracts/AccountNoteContract.cs csharp
using System;
namespace Gcg.DeveloperExamples.Migration
{
// A project-owned contract that both platform adapters can target.
public sealed class AccountNote
{
public int AccountId { get; }
public string Text { get; }
public long Revision { get; }
public AccountNote(int accountId, string text, long revision)
{
if (accountId <= 0 || revision < 1) throw new ArgumentOutOfRangeException();
if (text == null || text.Length > 2000) throw new ArgumentException("Invalid note.");
AccountId = accountId; Text = text; Revision = revision;
}
}
public static class NoteContractChecks
{
public static void RequireEquivalent(AccountNote source, AccountNote target)
{
if (source == null || target == null) throw new ArgumentNullException();
if (source.AccountId != target.AccountId ||
!string.Equals(source.Text, target.Text, StringComparison.Ordinal))
throw new InvalidOperationException("The migrated behavior differs.");
}
public static AccountNote ApplyEdit(AccountNote current,
int authorizedAccountId, long expectedRevision, string proposedText)
{
if (current.AccountId != authorizedAccountId)
throw new UnauthorizedAccessException();
if (current.Revision != expectedRevision)
throw new InvalidOperationException("Reload the current note before saving.");
return new AccountNote(current.AccountId, proposedText,
checked(current.Revision + 1));
}
}
}
// ApplyEdit is an immutable policy function. The persistence adapter must
// compare expectedRevision and save atomically; this function is not a lock.
Build one vertical slice on the target
Implement the smallest complete journey before porting adjacent features. Establish authentication and buying context, map the request into a project-owned contract, call the persistence boundary, and render its result. Keep vendor wire models at the edge so a changed response envelope does not propagate through the entire feature.
Use supported target APIs for Znode-owned entities. For project-owned relational data, identify the supported connectivity and hosting arrangement rather than assuming a legacy connection string can be carried forward. Record which services must exist before the Custom API and storefront can safely expose the feature. Include the hosted service connectivity and application configuration in the release checks.
Compare outcomes with deliberately different fixtures
Create two accounts, two actors, an absent note, a long note, and a conflicting edit. Run equivalent journeys against the declared source and target baselines. Compare observable behavior rather than implementation details. A test should explain why a difference is acceptable or flag it for correction.
Include loss of permission after a page was opened, an unavailable data service, malformed input, and a browser retry after an uncertain write. Check what is persisted as well as what appears on screen. Capture the observed account, saved value, and revision in the regression result.
- Pin SDK packages and deployment artifacts.
- Keep original and new response examples with the decision record.
- Make intentionally changed behavior visible to consuming teams.
Migrate the operating model too
The feature is not finished when its page works. Operators need to locate failures, distinguish a conflict from a dependency outage, and know whether a retry is safe. Release notes should describe configuration ownership, migration prerequisites, and the supported rollback boundary.
Repeat the vertical-slice method for the next journey. Shared adapters and conventions will emerge from demonstrated needs. This takes more care than a broad rename pass, but it produces a migration plan that can be estimated, reviewed, and accepted in business terms rather than measured only by the number of classes moved.
References and further reading
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.