Skip to content

One task on every transport and API level

Daniel Frantík edited this page Aug 29, 2026 · 8 revisions

One task on every transport and API level

This page holds one task fixed and varies the two axes around it: the API level you write against (low-level · ADO.NET-like · O/R mapper) and the transport the connection runs over (all 11 of them).

Each of the three sections below carries one tab per transport, and 29 of the 33 tabs are a complete, runnable program. Take the tab you need, paste it into a fresh console project, set Host/User/Password, and run it — nothing is elided, and each program's header comment states what it needs on the router, what goes in, what comes out, and what to watch out for on that particular transport.

The four tabs that are not programs are all in the low-level section: Rest, RestSsl, WinboxNative and WinboxNativeMac have no command language to write a raw command in, so that level does not exist there. Those tabs say why, and point at the two levels that do work on the same connection.

GitHub's wiki has no tab control, so each "tab" is a collapsible block — click a transport to open it.

Looking for create/read/update/delete written out per API level instead? CRUD examples for all APIs covers the four operations on /ip/address.

The task

Find the interface whose comment is managed-by-tik4net, keep its .id, and write a new comment back — addressing the row by that .id.

It is deliberately not "find and update in one call". Storing the .id between the two steps is what real code does when it reads a row, decides something about it, and writes back, and it is the point where the three API levels visibly differ: the low-level API hands you a .id string, the ADO.NET-like API hands you a row you take it from, and the O/R mapper hands you an object that carries it, so you never mention .id at all.

.id is what every write verb addresses. It is assigned by the router, is unique within a menu, and stays valid while the row exists — a name or a comment can be changed by someone else between your two calls.

The samples assume one interface carries that comment. SingleOrDefault / LoadSingleOrDefault are chosen for that reason: they return null for "no such row" and throw if the filter matched more than one, which is usually what you want when a marker comment is supposed to be unique.

Before you run anything

Two things every program needs, on top of the per-transport prerequisites in its header comment:

# 1 — the row the samples look for
/interface set [find default-name=ether1] comment="managed-by-tik4net"

# 2 — a user whose group carries "read" and "write", plus the policy for the transport
#     you are using. The per-transport policy is stated in each program; getting it wrong
#     does NOT always say so — see the table below.
/user/group print

Interface is used because every router has one and a comment is free to set and revert. Undo with /interface set [find default-name=ether1] comment="".

Which policy each transport needs — measured on RouterOS 7.24 by logging in with a user that had only read,write,test,local, then adding one policy at a time:

Transport Policy What a missing policy looks like
Api, ApiSsl api trap: std failure: not allowed (9)
Rest, RestSsl rest-api HTTP 401 Unauthorized — reads like a wrong password
Telnet, MacTelnet telnet Login failed, incorrect username or passwordalso reads like a wrong password
Ssh ssh Permission denied (password)
WinboxCli, WinboxCliMac, WinboxNative, WinboxNativeMac winbox refused: std failure: not allowed (9)

Note the third row: MAC-Telnet authenticates against the telnet policy, not a MAC-specific one, and three of the five messages are indistinguishable from a typo in the password. If a login fails on a password you are sure of, check the policy before checking the password.


Low-level API

the transport's own command language in, raw sentences out

This level is deliberately not transport-portable, and that is what makes it useful. What you write is sent to the router unchanged, so anything the router understands is reachable — scripting, /export, a menu tik4net has no entity for. "Unchanged" is the catch: the command has to already be in that transport's own language, and four transports have no such language at all.

  • Api / ApiSsl — API sentence words: /interface/print, ?comment=…, =.id=….
  • The CLI family (Telnet · Ssh · MacTelnet · WinboxCli · WinboxCliMac) — a RouterOS CLI line, the text you would type at a terminal. Nothing is translated: the API words above mean nothing here, and vice versa.
  • Rest / RestSsl, WinboxNative / WinboxNativeMac — an HTTP request and a numeric M2 message are shapes, not languages a caller can write. These four declare no RawCommand, so this level does not exist on them. Their tabs below say so and point at the two levels that do work there.

CallCommandSync lives on ITikRawSentenceConnection, not on ITikConnection, and there is no extension method that puts it there — a call that compiles on all eleven transports and works on seven would be worse than one the compiler rejects. Ask the transport's own factory for the connection and the method is simply present: setup.CreateApiConnection() returns ITikApiConnection, setup.CreateTelnetConnection() returns ITikCliConnection. The rules of this level are collected on Low-level API.

Seven complete, copy-and-run programs and four tabs explaining why there is nothing to run — pick your tab:

Api — binary API, TCP 8728
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      Low-level API
// TRANSPORT  Api — binary API, TCP 8728
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=api]                    # TCP 8728
//   /user/group set <group> policy=api,read,write,...     # the 'api' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     found .id=*2 name=ether1
//            updated, comment is now: managed-by-tik4net (checked)  (was: managed-by-tik4net)
//            — nothing carries the marker:   no interface carries comment=managed-by-tik4net
//            — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
//   CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection —
//   CreateApiConnection() returns ITikApiConnection, which has it.
//   The rows are API sentence words and are sent unchanged, so this program does NOT
//   port to a CLI transport — see the Telnet tab for the same task in CLI text.
//   A refused command arrives as a !trap SENTENCE here and does NOT throw. This is the
//   only family that behaves that way; the CLI transports throw instead.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Api;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        // CreateApiConnection() returns ITikApiConnection, which carries CallCommandSync.
        // A variable declared ITikConnection would not compile — see WATCH OUT.
        using (ITikApiConnection connection = setup.CreateApiConnection())
        {
            // ── FIND ────────────────────────────────────────────────────────────────────
            // '?' words are FILTERS. The router answers one !re sentence per matching
            // row and closes the reply with !done.
            var found = connection.CallCommandSync("/interface/print",
                "?comment=" + Marker);

            // The result is SENTENCES, not rows: only !re sentences carry data, and the
            // trailing !done is a sentence too — so select by type, never by position.
            var row = found.OfType<ITikReSentence>().SingleOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();                    // the ".id" word, router format "*2"
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // A field the router did not send is ABSENT, not empty — GetResponseField
            // throws for it, so read optional fields with GetResponseFieldOrDefault.
            string before = row.GetResponseFieldOrDefault("comment", "<unset>");

            // ── UPDATE ──────────────────────────────────────────────────────────────────
            // '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
            // update of that row rather than a second search.
            var reply = connection.CallCommandSync("/interface/set",
                "=.id=" + id,
                "=comment=" + NewComment);

            // On THIS transport a refusal is a !trap SENTENCE — the call does not throw,
            // so the reply has to be inspected. The CLI transports throw instead.
            var trap = reply.OfType<ITikTrapSentence>().FirstOrDefault();
            if (trap != null)
            {
                Console.WriteLine("update refused: " + trap.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment
                              + "  (was: " + before + ")");
        }
    }
}

Transport reference: Api.

ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      Low-level API
// TRANSPORT  ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /certificate add name=server common-name=<router> ; /certificate sign server
//   /ip/service set api-ssl certificate=server disabled=no    # TCP 8729
//   /user/group set <group> policy=api,read,write,...         # same 'api' policy as Api
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     found .id=*2 name=ether1
//            updated, comment is now: managed-by-tik4net (checked)  (was: managed-by-tik4net)
//            — nothing carries the marker:   no interface carries comment=managed-by-tik4net
//            — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
//   CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection —
//   CreateApiSslConnection() returns ITikApiConnection, which has it.
//   Use CertificateValidationCallback instead of AllowInvalidCertificate in production -
//   the flag disables validation entirely.
//   A refused command arrives as a !trap SENTENCE here too (binary API family).
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Api;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The lab router's certificate is self-signed. AllowInvalidCertificate is FALSE by
            // default since 4.0, and without this the open fails with TikConnectionSSLErrorException
            // ("The remote certificate was rejected...") before authentication is even attempted.
            AllowInvalidCertificate = true,
        };

        // CreateApiSslConnection() returns ITikApiConnection, which carries CallCommandSync.
        // A variable declared ITikConnection would not compile — see WATCH OUT.
        using (ITikApiConnection connection = setup.CreateApiSslConnection())
        {
            // ── FIND ────────────────────────────────────────────────────────────────────
            // '?' words are FILTERS. The router answers one !re sentence per matching
            // row and closes the reply with !done.
            var found = connection.CallCommandSync("/interface/print",
                "?comment=" + Marker);

            // The result is SENTENCES, not rows: only !re sentences carry data, and the
            // trailing !done is a sentence too — so select by type, never by position.
            var row = found.OfType<ITikReSentence>().SingleOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();                    // the ".id" word, router format "*2"
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // A field the router did not send is ABSENT, not empty — GetResponseField
            // throws for it, so read optional fields with GetResponseFieldOrDefault.
            string before = row.GetResponseFieldOrDefault("comment", "<unset>");

            // ── UPDATE ──────────────────────────────────────────────────────────────────
            // '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
            // update of that row rather than a second search.
            var reply = connection.CallCommandSync("/interface/set",
                "=.id=" + id,
                "=comment=" + NewComment);

            // On THIS transport a refusal is a !trap SENTENCE — the call does not throw,
            // so the reply has to be inspected. The CLI transports throw instead.
            var trap = reply.OfType<ITikTrapSentence>().FirstOrDefault();
            if (trap != null)
            {
                Console.WriteLine("update refused: " + trap.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment
                              + "  (was: " + before + ")");
        }
    }
}

Transport reference: ApiSsl.

Rest — HTTP REST API, TCP 80 (RouterOS 7.1+) · this level does not exist here

There is no low-level program for REST, because REST has no command language to write one in.

This level sends what you hand it unchanged, so it needs a dialect. A REST call is an HTTP request — a method, a URL path built from the command path, and a JSON body — assembled by the transport from the command you asked for. There is no line of text a caller could pass through verbatim, so RestConnection implements neither ITikRawSentenceConnection nor RawCommand:

using tik4net;
using tik4net.Rest;

var setup = new TikConnectionSetup(Host, User, Password);
using ITikRestConnection connection = setup.CreateRestConnection();

connection.Supports(TikConnectionCapability.RawCommand);    // false
// connection.CallCommandSync(...);                         // does not compile — no such method
connection.CreateRawCommand("/interface print");            // throws TikConnectionCapabilityNotSupportedException

Use one of the two levels above instead. Both build the request rather than passing one through, and both run on REST unchanged: the same task is in the ADO.NET-like Rest tab and the O/R mapper Rest tab.

Transport reference: Rest.

RestSsl — HTTP REST API over TLS, TCP 443 · this level does not exist here

Same as Rest: no command language, so no low-level program. TLS changes how the request travels, not what it is — an HTTP method, a path and a JSON body, with nothing for a caller to write verbatim. RestSsl therefore reports no RawCommand either, and CallCommandSync is not on ITikRestConnection.

using tik4net;
using tik4net.Rest;

var setup = new TikConnectionSetup(Host, User, Password) { AllowInvalidCertificate = true };
using ITikRestConnection connection = setup.CreateRestSslConnection();

connection.Supports(TikConnectionCapability.RawCommand);    // false

Use one of the two levels above instead: the ADO.NET-like RestSsl tab and the O/R mapper RestSsl tab do this task over the same connection.

Transport reference: RestSsl.

Telnet — plain-text CLI, TCP 23
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      Low-level API
// TRANSPORT  Telnet — plain-text CLI, TCP 23
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=telnet]                     # TCP 23
//   /user/group set <group> policy=telnet,read,write,...      # the 'telnet' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     found .id=*2 name=ether1
//            updated, comment is now: managed-by-tik4net (checked)  (was: managed-by-tik4net)
//            — nothing carries the marker:   no interface carries comment=managed-by-tik4net
//            — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
//   Credentials and every command travel in CLEAR TEXT. Prefer Ssh or WinboxCli on any
//   network you do not control.
//   A missing 'telnet' policy is reported by the router as "Login failed, incorrect username
//   or password" — it looks like a wrong password but is not (measured).
//   CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateTelnetConnection()
//   returns ITikCliConnection, which has it.
//   The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
//   "=.id=...") are NOT translated here. Several rows are joined with one space, so the
//   line may be written in one piece or split.
//   A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
//   script context, so a bare 'print as-value' at a terminal comes back as an empty line.
//   'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
//   row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
//   mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
//   'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
//   A refusal is classified from the output TEXT, because raw mode does not know which verb
//   was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
//   a way the classifier does not recognise can pass as success. The two levels above are
//   verb-aware and do not have that gap.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Telnet;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        // CreateTelnetConnection() returns ITikCliConnection, which carries CallCommandSync.
        // A variable declared ITikConnection would not compile — see WATCH OUT.
        using (ITikCliConnection connection = setup.CreateTelnetConnection())
        {
            // ── FIND ────────────────────────────────────────────────────────────────────
            // A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
            // ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
            // terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
            // the value in 'where' is quoted so a marker with a space or a '/' still parses.
            var found = connection.CallCommandSync(
                ":put [/interface print detail as-value where comment=\"" + Marker + "\"]");

            // as-value output comes back as one !re sentence per record, closed by a !done —
            // so select by type, never by position.
            var row = found.OfType<ITikReSentence>().SingleOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();                    // the ".id" field, router format "*2"
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // A field the router did not send is ABSENT, not empty — GetResponseField
            // throws for it, so read optional fields with GetResponseFieldOrDefault.
            string before = row.GetResponseFieldOrDefault("comment", "<unset>");

            // ── UPDATE ──────────────────────────────────────────────────────────────────
            // The CLI addresses the row by that same .id, written as a bare selector.
            // A write that succeeds prints nothing at all; a refusal arrives as an exception.
            try
            {
                connection.CallCommandSync(
                    "/interface set " + id + " comment=\"" + NewComment + "\"");
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment
                              + "  (was: " + before + ")");
        }
    }
}

Transport reference: Telnet.

Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      Low-level API
// TRANSPORT  Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=ssh]                        # TCP 22
//   /user/group set <group> policy=ssh,read,write,...         # the 'ssh' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     found .id=*2 name=ether1
//            updated, comment is now: managed-by-tik4net (checked)  (was: managed-by-tik4net)
//            — nothing carries the marker:   no interface carries comment=managed-by-tik4net
//            — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
//   The ONLY transport that ships in its own NuGet package (tik4net.ssh, which pulls in
//   Renci.SshNet). CreateSshConnection() lives in that package and needs no registration;
//   Tik4NetSsh.Register() is only for the ConnectionFactory route,
//   setup.Create(TikConnectionType.Ssh), which throws NotImplementedException without it.
//   A missing 'ssh' policy surfaces as "Permission denied (password)" (measured).
//   CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateSshConnection()
//   returns ITikCliConnection, which has it.
//   The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
//   "=.id=...") are NOT translated here. Several rows are joined with one space, so the
//   line may be written in one piece or split.
//   A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
//   script context, so a bare 'print as-value' at a terminal comes back as an empty line.
//   'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
//   row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
//   mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
//   'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
//   A refusal is classified from the output TEXT, because raw mode does not know which verb
//   was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
//   a way the classifier does not recognise can pass as success. The two levels above are
//   verb-aware and do not have that gap.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Ssh;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        // CreateSshConnection() returns ITikCliConnection, which carries CallCommandSync.
        // A variable declared ITikConnection would not compile — see WATCH OUT.
        using (ITikCliConnection connection = setup.CreateSshConnection())
        {
            // ── FIND ────────────────────────────────────────────────────────────────────
            // A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
            // ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
            // terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
            // the value in 'where' is quoted so a marker with a space or a '/' still parses.
            var found = connection.CallCommandSync(
                ":put [/interface print detail as-value where comment=\"" + Marker + "\"]");

            // as-value output comes back as one !re sentence per record, closed by a !done —
            // so select by type, never by position.
            var row = found.OfType<ITikReSentence>().SingleOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();                    // the ".id" field, router format "*2"
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // A field the router did not send is ABSENT, not empty — GetResponseField
            // throws for it, so read optional fields with GetResponseFieldOrDefault.
            string before = row.GetResponseFieldOrDefault("comment", "<unset>");

            // ── UPDATE ──────────────────────────────────────────────────────────────────
            // The CLI addresses the row by that same .id, written as a bare selector.
            // A write that succeeds prints nothing at all; a refusal arrives as an exception.
            try
            {
                connection.CallCommandSync(
                    "/interface set " + id + " comment=\"" + NewComment + "\"");
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment
                              + "  (was: " + before + ")");
        }
    }
}

Transport reference: Ssh.

MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      Low-level API
// TRANSPORT  MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server set allowed-interface-list=all           # or a list holding your segment
//   /user/group set <group> policy=telnet,read,write,...      # MAC-Telnet uses 'telnet' (measured)
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     found .id=*2 name=ether1
//            updated, comment is now: managed-by-tik4net (checked)  (was: managed-by-tik4net)
//            — nothing carries the marker:   no interface carries comment=managed-by-tik4net
//            — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
//   No IP route to the router is needed — this is the recovery/bootstrap transport, and a
//   router with NO IP ADDRESS AT ALL is reachable: drop Host entirely and address the setup
//   with TikRouterAddress.FromMac(RouterMacAddress). The MAC then has to be given (MNDP
//   answers "the MAC of the router at this address", which there is no address to ask about),
//   and the local interface is found by trying each adapter until one is answered.
//   Host, when passed, still selects the local network path, so keep it pointing at the
//   router's segment; RouterMac is what addresses the router.
//   If your machine has several NICs, broadcast can leave through the wrong one — MNDP will
//   still answer, which makes the failure look like the router's fault. See the linked page.
//   Noticeably slower than the IP transports.
//   CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateMacTelnetConnection()
//   returns ITikMacCliConnection, which has it.
//   The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
//   "=.id=...") are NOT translated here. Several rows are joined with one space, so the
//   line may be written in one piece or split.
//   A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
//   script context, so a bare 'print as-value' at a terminal comes back as an empty line.
//   'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
//   row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
//   mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
//   'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
//   A refusal is classified from the output TEXT, because raw mode does not know which verb
//   was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
//   a way the classifier does not recognise can pass as success. The two levels above are
//   verb-aware and do not have that gap.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.MacTelnet;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC. Leave this out (or null) and MNDP discovers it — up to 5 s.
            RouterMac = RouterMacAddress,
        };

        // CreateMacTelnetConnection() returns ITikMacCliConnection, which carries CallCommandSync.
        // A variable declared ITikConnection would not compile — see WATCH OUT.
        using (ITikMacCliConnection connection = setup.CreateMacTelnetConnection())
        {
            // ── FIND ────────────────────────────────────────────────────────────────────
            // A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
            // ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
            // terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
            // the value in 'where' is quoted so a marker with a space or a '/' still parses.
            var found = connection.CallCommandSync(
                ":put [/interface print detail as-value where comment=\"" + Marker + "\"]");

            // as-value output comes back as one !re sentence per record, closed by a !done —
            // so select by type, never by position.
            var row = found.OfType<ITikReSentence>().SingleOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();                    // the ".id" field, router format "*2"
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // A field the router did not send is ABSENT, not empty — GetResponseField
            // throws for it, so read optional fields with GetResponseFieldOrDefault.
            string before = row.GetResponseFieldOrDefault("comment", "<unset>");

            // ── UPDATE ──────────────────────────────────────────────────────────────────
            // The CLI addresses the row by that same .id, written as a bare selector.
            // A write that succeeds prints nothing at all; a refusal arrives as an exception.
            try
            {
                connection.CallCommandSync(
                    "/interface set " + id + " comment=\"" + NewComment + "\"");
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment
                              + "  (was: " + before + ")");
        }
    }
}

Transport reference: MacTelnet.

WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      Low-level API
// TRANSPORT  WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=winbox]                     # TCP 8291
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     found .id=*2 name=ether1
//            updated, comment is now: managed-by-tik4net (checked)  (was: managed-by-tik4net)
//            — nothing carries the marker:   no interface carries comment=managed-by-tik4net
//            — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
//   Encrypted (EC-SRP5 + AES) with no certificates to manage — the practical alternative to
//   Telnet when you cannot use TLS.
//   A missing 'winbox' policy is refused with "std failure: not allowed (9)" (measured).
//   CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateWinboxCliConnection()
//   returns ITikCliConnection, which has it.
//   The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
//   "=.id=...") are NOT translated here. Several rows are joined with one space, so the
//   line may be written in one piece or split.
//   A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
//   script context, so a bare 'print as-value' at a terminal comes back as an empty line.
//   'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
//   row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
//   mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
//   'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
//   A refusal is classified from the output TEXT, because raw mode does not know which verb
//   was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
//   a way the classifier does not recognise can pass as success. The two levels above are
//   verb-aware and do not have that gap.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.WinboxCli;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        // CreateWinboxCliConnection() returns ITikCliConnection, which carries CallCommandSync.
        // A variable declared ITikConnection would not compile — see WATCH OUT.
        using (ITikCliConnection connection = setup.CreateWinboxCliConnection())
        {
            // ── FIND ────────────────────────────────────────────────────────────────────
            // A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
            // ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
            // terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
            // the value in 'where' is quoted so a marker with a space or a '/' still parses.
            var found = connection.CallCommandSync(
                ":put [/interface print detail as-value where comment=\"" + Marker + "\"]");

            // as-value output comes back as one !re sentence per record, closed by a !done —
            // so select by type, never by position.
            var row = found.OfType<ITikReSentence>().SingleOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();                    // the ".id" field, router format "*2"
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // A field the router did not send is ABSENT, not empty — GetResponseField
            // throws for it, so read optional fields with GetResponseFieldOrDefault.
            string before = row.GetResponseFieldOrDefault("comment", "<unset>");

            // ── UPDATE ──────────────────────────────────────────────────────────────────
            // The CLI addresses the row by that same .id, written as a bare selector.
            // A write that succeeds prints nothing at all; a refusal arrives as an exception.
            try
            {
                connection.CallCommandSync(
                    "/interface set " + id + " comment=\"" + NewComment + "\"");
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment
                              + "  (was: " + before + ")");
        }
    }
}

Transport reference: WinboxCli.

WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      Low-level API
// TRANSPORT  WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server/mac-winbox set allowed-interface-list=all
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy (measured)
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     found .id=*2 name=ether1
//            updated, comment is now: managed-by-tik4net (checked)  (was: managed-by-tik4net)
//            — nothing carries the marker:   no interface carries comment=managed-by-tik4net
//            — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
//   Encrypted AND routeless at once — and the slowest of the eleven. Use it when you need
//   both properties; otherwise WinboxCli or MacTelnet is faster.
//   CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateWinboxCliMacConnection()
//   returns ITikMacCliConnection, which has it.
//   The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
//   "=.id=...") are NOT translated here. Several rows are joined with one space, so the
//   line may be written in one piece or split.
//   A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
//   script context, so a bare 'print as-value' at a terminal comes back as an empty line.
//   'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
//   row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
//   mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
//   'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
//   A refusal is classified from the output TEXT, because raw mode does not know which verb
//   was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
//   a way the classifier does not recognise can pass as success. The two levels above are
//   verb-aware and do not have that gap.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.WinboxCliMac;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC; omit for MNDP discovery (up to 5 s).
            RouterMac = RouterMacAddress,
        };

        // CreateWinboxCliMacConnection() returns ITikMacCliConnection, which carries CallCommandSync.
        // A variable declared ITikConnection would not compile — see WATCH OUT.
        using (ITikMacCliConnection connection = setup.CreateWinboxCliMacConnection())
        {
            // ── FIND ────────────────────────────────────────────────────────────────────
            // A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
            // ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
            // terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
            // the value in 'where' is quoted so a marker with a space or a '/' still parses.
            var found = connection.CallCommandSync(
                ":put [/interface print detail as-value where comment=\"" + Marker + "\"]");

            // as-value output comes back as one !re sentence per record, closed by a !done —
            // so select by type, never by position.
            var row = found.OfType<ITikReSentence>().SingleOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();                    // the ".id" field, router format "*2"
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // A field the router did not send is ABSENT, not empty — GetResponseField
            // throws for it, so read optional fields with GetResponseFieldOrDefault.
            string before = row.GetResponseFieldOrDefault("comment", "<unset>");

            // ── UPDATE ──────────────────────────────────────────────────────────────────
            // The CLI addresses the row by that same .id, written as a bare selector.
            // A write that succeeds prints nothing at all; a refusal arrives as an exception.
            try
            {
                connection.CallCommandSync(
                    "/interface set " + id + " comment=\"" + NewComment + "\"");
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment
                              + "  (was: " + before + ")");
        }
    }
}

Transport reference: WinboxCliMac.

WinboxNative — structured WinBox M2, TCP 8291 · this level does not exist here

There is no low-level program for native WinBox, because there is no text to send. This transport speaks the same structured M2 messages the WinBox GUI does: a numeric handler, a command number and fields addressed by numeric key, resolved from the router's own catalog. That is a request shape, not a command language — nothing a caller could type and have passed through — so WinboxNativeConnection implements neither ITikRawSentenceConnection nor RawCommand:

using tik4net;
using tik4net.WinboxNative;

var setup = new TikConnectionSetup(Host, User, Password);
using ITikWinboxNativeConnection connection = setup.CreateWinboxNativeConnection();

connection.Supports(TikConnectionCapability.RawCommand);    // false
// connection.CallCommandSync(...);                         // does not compile — no such method
connection.CreateRawCommand("/interface print");            // throws TikConnectionCapabilityNotSupportedException

Use one of the two levels above instead: the ADO.NET-like WinboxNative tab and the O/R mapper WinboxNative tab do this task over the same connection, with the path and fields resolved for you. A path WinBox has no window for is refused with TikPathNotMappedException rather than answered with wrong data.

Transport reference: WinboxNative.

WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561 · this level does not exist here

Same as WinboxNative: structured M2, so nothing to write raw. The MAC layer only changes how the M2 message is carried; the message is still numeric handlers and field keys, and no RawCommand is declared.

using tik4net;
using tik4net.WinboxNativeMac;

var setup = new TikConnectionSetup(Host, User, Password) { RouterMac = "AA:BB:CC:DD:EE:FF" };
using ITikWinboxNativeMacConnection connection = setup.CreateWinboxNativeMacConnection();

connection.Supports(TikConnectionCapability.RawCommand);    // false

Use one of the two levels above instead: the ADO.NET-like WinboxNativeMac tab and the O/R mapper WinboxNativeMac tab. Note there what is said about the idle session: an M2 session over the MAC layer is dropped by the router after roughly 60 s idle, a read is reopened and reissued automatically, a write never is.

Transport reference: WinboxNativeMac.

ADO.NET-like API

commands, parameters, typed Execute* calls

One complete, copy-and-run program per transport — pick your tab:

Api — binary API, TCP 8728
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  Api — binary API, TCP 8728
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=api]                    # TCP 8728
//   /user/group set <group> policy=api,read,write,...     # the 'api' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   A refused command arrives as a !trap SENTENCE on this transport and does NOT throw at
//   the low-level API. This is the only family that behaves that way — see the trap note
//   in the sample below.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Api))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: Api.

ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /certificate add name=server common-name=<router> ; /certificate sign server
//   /ip/service set api-ssl certificate=server disabled=no    # TCP 8729
//   /user/group set <group> policy=api,read,write,...         # same 'api' policy as Api
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Use CertificateValidationCallback instead of AllowInvalidCertificate in production —
//   the flag disables validation entirely.
//   A refused command arrives as a !trap SENTENCE here too (binary API family).
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The lab router's certificate is self-signed. AllowInvalidCertificate is FALSE by
            // default since 4.0, and without this the open fails with TikConnectionSSLErrorException
            // ("The remote certificate was rejected...") before authentication is even attempted.
            AllowInvalidCertificate = true,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.ApiSsl))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: ApiSsl.

Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=www]                        # TCP 80
//   /user/group set <group> policy=rest-api,read,write,...    # 'rest-api', NOT 'api'
//   # RouterOS 7.1 or newer — the REST endpoint does not exist on 6.x
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   A missing 'rest-api' policy surfaces as HTTP 401 Unauthorized, which reads exactly like
//   a wrong password (measured).
//   REST buffers the whole response before returning it, so it is not a streaming transport
//   and it has no Safe Mode.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Rest))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: Rest.

RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /certificate add name=server common-name=<router> ; /certificate sign server
//   /ip/service set www-ssl certificate=server disabled=no    # TCP 443
//   /user/group set <group> policy=rest-api,read,write,...
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   If 'reverse-proxy' also listens on 443, only one of the two can own the port.
//   Same buffering and Safe Mode limits as Rest.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // Self-signed lab certificate — see the ApiSsl tab; false by default since 4.0.
            AllowInvalidCertificate = true,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.RestSsl))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: RestSsl.

Telnet — plain-text CLI, TCP 23
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  Telnet — plain-text CLI, TCP 23
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=telnet]                     # TCP 23
//   /user/group set <group> policy=telnet,read,write,...      # the 'telnet' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Credentials and every command travel in CLEAR TEXT. Prefer Ssh or WinboxCli on any
//   network you do not control.
//   A missing 'telnet' policy is reported by the router as "Login failed, incorrect username
//   or password" — it looks like a wrong password but is not (measured).
//   On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
//   columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
//   'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
//   link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
//   level asks for all of this itself and is unaffected.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Telnet))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);
            // Ask for the full column set (see WATCH OUT). 'detail' is a name-value
            // word, not a filter, so its format is given explicitly.
            findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: Telnet.

Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=ssh]                        # TCP 22
//   /user/group set <group> policy=ssh,read,write,...         # the 'ssh' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   The ONLY transport that needs a registration call, because it ships in its own NuGet
//   package (tik4net.ssh, which pulls in Renci.SshNet). Without Register() the Create call
//   throws NotImplementedException.
//   A missing 'ssh' policy surfaces as "Permission denied (password)" (measured).
//   On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
//   columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
//   'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
//   link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
//   level asks for all of this itself and is unaffected.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Ssh;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        Tik4NetSsh.Register();   // once at startup — see the note above the sample

        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Ssh))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);
            // Ask for the full column set (see WATCH OUT). 'detail' is a name-value
            // word, not a filter, so its format is given explicitly.
            findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: Ssh.

MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server set allowed-interface-list=all           # or a list holding your segment
//   /user/group set <group> policy=telnet,read,write,...      # MAC-Telnet uses 'telnet' (measured)
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   No IP route to the router is needed — this is the recovery/bootstrap transport, and a
//   router with NO IP ADDRESS AT ALL is reachable: drop Host entirely and address the setup
//   with TikRouterAddress.FromMac(RouterMacAddress). The MAC then has to be given (MNDP
//   answers "the MAC of the router at this address", which there is no address to ask about),
//   and the local interface is found by trying each adapter until one is answered.
//   Host, when passed, still selects the local network path, so keep it pointing at the
//   router's segment; RouterMac is what addresses the router.
//   If your machine has several NICs, broadcast can leave through the wrong one — MNDP will
//   still answer, which makes the failure look like the router's fault. See the linked page.
//   Noticeably slower than the IP transports.
//   On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
//   columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
//   'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
//   link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
//   level asks for all of this itself and is unaffected.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC. Leave this out (or null) and MNDP discovers it — up to 5 s.
            RouterMac = RouterMacAddress,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.MacTelnet))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);
            // Ask for the full column set (see WATCH OUT). 'detail' is a name-value
            // word, not a filter, so its format is given explicitly.
            findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: MacTelnet.

WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=winbox]                     # TCP 8291
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Encrypted (EC-SRP5 + AES) with no certificates to manage — the practical alternative to
//   Telnet when you cannot use TLS.
//   A missing 'winbox' policy is refused with "std failure: not allowed (9)" (measured).
//   On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
//   columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
//   'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
//   link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
//   level asks for all of this itself and is unaffected.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCli))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);
            // Ask for the full column set (see WATCH OUT). 'detail' is a name-value
            // word, not a filter, so its format is given explicitly.
            findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: WinboxCli.

WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server/mac-winbox set allowed-interface-list=all
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy (measured)
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Encrypted AND routeless at once — and the slowest of the eleven. Use it when you need
//   both properties; otherwise WinboxCli or MacTelnet is faster.
//   On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
//   columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
//   'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
//   link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
//   level asks for all of this itself and is unaffected.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC; omit for MNDP discovery (up to 5 s).
            RouterMac = RouterMacAddress,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCliMac))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);
            // Ask for the full column set (see WATCH OUT). 'detail' is a name-value
            // word, not a filter, so its format is given explicitly.
            findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: WinboxCliMac.

WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=winbox]                     # TCP 8291
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Not a CLI: the call becomes the same structured M2 request the WinBox GUI makes, with
//   fields addressed by numeric key.
//   A path or field WinBox has no window for cannot be reached — you get
//   TikPathNotMappedException rather than wrong data. The same path usually works over
//   Api or a CLI transport.
//   Rows carry a few native-only fields the API does not report.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNative))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: WinboxNative.

WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      ADO.NET-like API
// TRANSPORT  WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server/mac-winbox set allowed-interface-list=all
//   /user/group set <group> policy=winbox,read,write,...
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Everything in the WinboxNative tab applies, over the MAC carrier.
//   An M2 session over the MAC layer is dropped by the router after roughly 60 s idle; a
//   read is reopened and reissued automatically, a write never is.
// ==========================================================================

using System;
using System.Linq;
using tik4net;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC; omit for MNDP discovery (up to 5 s).
            RouterMac = RouterMacAddress,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNativeMac))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The parameter FORMAT is not stated here — the Execute* call decides it.
            // ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
            ITikCommand findCmd = connection.CreateCommandAndParameters(
                "/interface/print", "comment", Marker);

            // Returns null for "no such row" and THROWS if the filter matched more than
            // one — which is what you want for a marker comment that must be unique.
            ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
            if (row == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            string id = row.GetId();
            Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));

            // ── UPDATE ──────────────────────────────────────────────────────────
            // ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
            // =.id=… — which is why the id can be passed as an ordinary parameter.
            ITikCommand updateCmd = connection.CreateCommandAndParameters(
                "/interface/set",
                "comment", NewComment,
                TikSpecialProperties.Id, id);

            // Identical error handling on all eleven transports: a refusal throws.
            try
            {
                updateCmd.ExecuteNonQuery();
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + NewComment);
        }
    }
}

Transport reference: WinboxNativeMac.

High-level O/R mapper

typed entities, the object carries the .id

One complete, copy-and-run program per transport — pick your tab:

Api — binary API, TCP 8728
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  Api — binary API, TCP 8728
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=api]                    # TCP 8728
//   /user/group set <group> policy=api,read,write,...     # the 'api' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   A refused command arrives as a !trap SENTENCE on this transport and does NOT throw at
//   the low-level API. This is the only family that behaves that way — see the trap note
//   in the sample below.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Api))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: Api.

ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /certificate add name=server common-name=<router> ; /certificate sign server
//   /ip/service set api-ssl certificate=server disabled=no    # TCP 8729
//   /user/group set <group> policy=api,read,write,...         # same 'api' policy as Api
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Use CertificateValidationCallback instead of AllowInvalidCertificate in production —
//   the flag disables validation entirely.
//   A refused command arrives as a !trap SENTENCE here too (binary API family).
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The lab router's certificate is self-signed. AllowInvalidCertificate is FALSE by
            // default since 4.0, and without this the open fails with TikConnectionSSLErrorException
            // ("The remote certificate was rejected...") before authentication is even attempted.
            AllowInvalidCertificate = true,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.ApiSsl))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: ApiSsl.

Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=www]                        # TCP 80
//   /user/group set <group> policy=rest-api,read,write,...    # 'rest-api', NOT 'api'
//   # RouterOS 7.1 or newer — the REST endpoint does not exist on 6.x
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   A missing 'rest-api' policy surfaces as HTTP 401 Unauthorized, which reads exactly like
//   a wrong password (measured).
//   REST buffers the whole response before returning it, so it is not a streaming transport
//   and it has no Safe Mode.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Rest))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: Rest.

RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /certificate add name=server common-name=<router> ; /certificate sign server
//   /ip/service set www-ssl certificate=server disabled=no    # TCP 443
//   /user/group set <group> policy=rest-api,read,write,...
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   If 'reverse-proxy' also listens on 443, only one of the two can own the port.
//   Same buffering and Safe Mode limits as Rest.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // Self-signed lab certificate — see the ApiSsl tab; false by default since 4.0.
            AllowInvalidCertificate = true,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.RestSsl))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: RestSsl.

Telnet — plain-text CLI, TCP 23
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  Telnet — plain-text CLI, TCP 23
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=telnet]                     # TCP 23
//   /user/group set <group> policy=telnet,read,write,...      # the 'telnet' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Credentials and every command travel in CLEAR TEXT. Prefer Ssh or WinboxCli on any
//   network you do not control.
//   A missing 'telnet' policy is reported by the router as "Login failed, incorrect username
//   or password" — it looks like a wrong password but is not (measured).
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Telnet))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: Telnet.

Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=ssh]                        # TCP 22
//   /user/group set <group> policy=ssh,read,write,...         # the 'ssh' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   The ONLY transport that needs a registration call, because it ships in its own NuGet
//   package (tik4net.ssh, which pulls in Renci.SshNet). Without Register() the Create call
//   throws NotImplementedException.
//   A missing 'ssh' policy surfaces as "Permission denied (password)" (measured).
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
using tik4net.Ssh;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        Tik4NetSsh.Register();   // once at startup — see the note above the sample

        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.Ssh))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: Ssh.

MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server set allowed-interface-list=all           # or a list holding your segment
//   /user/group set <group> policy=telnet,read,write,...      # MAC-Telnet uses 'telnet' (measured)
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   No IP route to the router is needed — this is the recovery/bootstrap transport, and a
//   router with NO IP ADDRESS AT ALL is reachable: drop Host entirely and address the setup
//   with TikRouterAddress.FromMac(RouterMacAddress). The MAC then has to be given (MNDP
//   answers "the MAC of the router at this address", which there is no address to ask about),
//   and the local interface is found by trying each adapter until one is answered.
//   Host, when passed, still selects the local network path, so keep it pointing at the
//   router's segment; RouterMac is what addresses the router.
//   If your machine has several NICs, broadcast can leave through the wrong one — MNDP will
//   still answer, which makes the failure look like the router's fault. See the linked page.
//   Noticeably slower than the IP transports.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC. Leave this out (or null) and MNDP discovers it — up to 5 s.
            RouterMac = RouterMacAddress,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.MacTelnet))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: MacTelnet.

WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=winbox]                     # TCP 8291
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Encrypted (EC-SRP5 + AES) with no certificates to manage — the practical alternative to
//   Telnet when you cannot use TLS.
//   A missing 'winbox' policy is refused with "std failure: not allowed (9)" (measured).
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCli))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: WinboxCli.

WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server/mac-winbox set allowed-interface-list=all
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy (measured)
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Encrypted AND routeless at once — and the slowest of the eleven. Use it when you need
//   both properties; otherwise WinboxCli or MacTelnet is faster.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC; omit for MNDP discovery (up to 5 s).
            RouterMac = RouterMacAddress,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCliMac))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: WinboxCliMac.

WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /ip/service enable [find name=winbox]                     # TCP 8291
//   /user/group set <group> policy=winbox,read,write,...      # the 'winbox' policy is required
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Not a CLI: the call becomes the same structured M2 request the WinBox GUI makes, with
//   fields addressed by numeric key.
//   A path or field WinBox has no window for cannot be reached — you get
//   TikPathNotMappedException rather than wrong data. The same path usually works over
//   Api or a CLI transport.
//   Rows carry a few native-only fields the API does not report.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host       = "192.168.88.1";             // the router's address
    const string User       = "admin";
    const string Password   = "";
    const string Marker     = "managed-by-tik4net";       // the comment that identifies the row
    const string NewComment = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password);

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNative))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: WinboxNative.

WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// TASK       find the interface commented "managed-by-tik4net", keep its .id,
//            then write a new comment back addressing the row by that .id
// LEVEL      High-level O/R mapper
// TRANSPORT  WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
//   /tool/mac-server/mac-winbox set allowed-interface-list=all
//   /user/group set <group> policy=winbox,read,write,...
//   # your machine must be in the router's LAYER-2 broadcast domain
//   /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT      Host / User / Password — router coordinates
//            RouterMacAddress       — the router's MAC; omit it to discover by MNDP (needs Host)
//            Marker                 — the comment that identifies the row
//            NewComment             — what gets written back
//
// OUTPUT     prints  found .id=*2 name=ether1 mtu=1500
//                    updated, comment is now: managed-by-tik4net (checked)
//            prints  "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
//   Everything in the WinboxNative tab applies, over the MAC carrier.
//   An M2 session over the MAC layer is dropped by the router after roughly 60 s idle; a
//   read is reopened and reissued automatically, a write never is.
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

class Program
{
    const string Host             = "192.168.88.1";       // the router's address
    const string User             = "admin";
    const string Password         = "";
    const string RouterMacAddress = "AA:BB:CC:DD:EE:FF";  // omit to discover the router by MNDP
    const string Marker           = "managed-by-tik4net"; // the comment that identifies the row
    const string NewComment       = "managed-by-tik4net (checked)";

    static void Main()
    {
        var setup = new TikConnectionSetup(Host, User, Password)
        {
            // The router's MAC; omit for MNDP discovery (up to 5 s).
            RouterMac = RouterMacAddress,
        };

        using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNativeMac))
        {
            // ── FIND ────────────────────────────────────────────────────────────
            // The entity's own metadata decides what is asked for, so the read is
            // complete on every transport — including the CLI ones, where a hand-written
            // print would return the summary columns only.
            Interface iface = connection.LoadSingleOrDefault<Interface>(
                connection.CreateParameter("comment", Marker));

            // null = nothing matched. LoadSingle<T> is the same query that throws instead;
            // either way, more than one match throws.
            if (iface == null)
            {
                Console.WriteLine("no interface carries comment=" + Marker);
                return;
            }

            // Properties are nullable since 4.0: a field the router did not send is null,
            // not "" or 0.
            Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
                              + " mtu=" + iface.Mtu);

            // ── UPDATE ──────────────────────────────────────────────────────────
            // No .id is spelled out: the entity carries it and Save() addresses the row
            // with it. Change tracking diffs against the load-time snapshot, so what goes
            // on the wire is /interface/set =.id=… =comment=… — one field, not the row.
            // A Save with nothing changed makes no call at all.
            iface.Comment = NewComment;
            try
            {
                connection.Save(iface);
            }
            catch (TikCommandException ex)
            {
                Console.WriteLine("update refused: " + ex.Message);
                return;
            }

            Console.WriteLine("updated, comment is now: " + iface.Comment);
        }
    }
}

Transport reference: WinboxNativeMac.

---

What actually differs

Measured on RouterOS 7.24 by running every program on this page against a live router — 29 of 29 found the row and wrote the comment through. (29, not 33: the low level does not exist on Rest, RestSsl, WinboxNative and WinboxNativeMac, so those four tabs are an explanation rather than a program.) So the table is about what the reply looks like, not about whether the task works somewhere and not elsewhere:

Api · ApiSsl Rest · RestSsl CLI family
(Telnet · Ssh · MacTelnet · WinboxCli · WinboxCliMac)
WinboxNative · WinboxNativeMac
Filter by comment ✔ (where comment=…) ✔ (filtered client-side)
.id returned *2 *2 *2 *2
Update by .id
Fields on a bare print all all summary columns only all, plus native-only extras
The low-level API at all API sentence words not available RouterOS CLI text not available
Refusal at the low-level API ITikTrapSentence throws
Refusal at the other two levels throws throws throws throws

Three rows deserve more than a tick.

The low level exists only where the transport has a command language. Api/ApiSsl have sentence words and the CLI family has the RouterOS command line; REST has an HTTP request shape and native WinBox a numeric M2 message, neither of which a caller can write out, so they declare no RawCommand and CallCommandSync is not on their connection types at all. The two levels above are portable across all eleven.

The trap sentence exists on Api/ApiSsl only. They speak the binary protocol, where a refusal really is a !trap sentence on the wire, so the low-level API hands it back and the call does not throw. Every other transport emulates the sentence protocol on top of a CLI/REST/M2 exchange, has no !trap to hand back, and throws instead. Code written against sentence inspection therefore silently stops checking anything when you move it to another transport — which is why the low-level tabs differ between the two families, and why the ADO.NET-like level and above are the portable way to write error handling.

A bare print on a CLI transport returns the summary columns. It becomes print as-value, which for a commented /interface row is 8 fields against the binary API's 28 — mtu, default-name, vrf and the link times are simply absent. Adding detail brings those back (14 fields), but not the byte counters: those come from print stats, which is a separate query. The CLI tabs at the low level add detail themselves. The O/R mapper is unaffected: an entity declares IncludeDetails / IncludeCliStats and the mapper adds both markers to every load, so a typed Interface is complete on all eleven transports.

The parts that do not change

At the ADO.NET-like level and above: everything except the Create(...) line and the transport's own prerequisites. Within one of those two levels the body is the same code on all eleven transports. That is the point of ITikConnection: pick the transport at the edge of your program, and the rest of it does not know which one it got.

The low level is the exception, by design — it is where you go because you want the transport's own language — so its programs differ between the API family and the CLI family, and four transports have no such level at all.

Options are applied through TikConnectionSetup rather than set on the concrete connection type — a transport declares what it can honour, so RouterMac on a TCP transport or AllowInvalidCertificate on a plaintext one is simply not applied rather than silently half-applied.

How each call is translated per transport is written up in Command translation on non-API transports; which transport can do what at all is the matrix in Connection types & capabilities.

See also

Clone this wiki locally