Znode 9 to 10: migrate behavior before translating classes

Preserve the rules of an account-note workflow while moving from Framework 4.8 to Znode 10, using one named model and separate platform adapters.

MigrationGuideAdvancedGCG engineering guide
In this guide
  1. Observed behavior
  2. Explicit contract
  3. Target adapter
  4. Verified journey
Preserve the business journey while deliberately replacing platform-specific mechanics.

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.

Record the runtime transition explicitly: Znode 9 custom assemblies target .NET Framework 4.8, while the reviewed Znode 10 CustomAPISDK targets .NET 8 and ASP.NET Core. Rebuild System.Web controllers, classic MVC Razor views, Web.config behavior, and host-specific dependency registration for the new architecture. The Znode 10 Next.js storefront has its own Node.js and React dependencies. A C# language-version setting does not make a .NET 8 assembly loadable in Framework 4.8.

Compare a complete behavior contract

Map the Znode 9 and Znode 10 responses into the same AccountNote model. Its AccountId, Text, and Revision properties make the business values visible without carrying either SDK's response classes through the application. Compile the helper separately for each runtime and keep platform-specific reads and writes in adapters.

RequireEquivalent compares account identity and exact text across the source and target. It intentionally does not compare their revision numbers, which may be assigned differently. ApplyEdit checks the authorized account and expected revision, then returns a new AccountNote with the proposed text and the next revision.

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.

If the current note belongs to account 24 at revision 5, an authorized edit expecting revision 5 returns revision 6. An edit expecting revision 4 fails and asks the reader to reload. ApplyEdit does not save or lock the record: the persistence adapter must compare revision 5 and write revision 6 as one atomic operation so another writer cannot slip between those steps.

Build one vertical slice on the target

Implement one complete note-edit journey on the target: authenticate the user, establish the buying account, read the note, accept an authorized edit, save conditionally, and display the result. Keeping that full path small reveals missing permissions or deployment prerequisites earlier than translating every class at once.

Use supported clients for Znode-owned entities. For application-owned relational data, verify the target hosting and connectivity arrangement. Record the services and configuration that must be available before the Custom API and storefront expose the feature.

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

Bring your next engineering question.

Plan a Znode modernization

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

Repeatable Znode test data: stable keys, readback, and no-op reruns

Next
Next

Plan a coordinated Custom API, data, and storefront release