Skip to content

One task on every transport and API level

Daniel Frantík edited this page Aug 21, 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).

Looking for create/read/update/delete written out per API level instead? CRUD examples for all APIs covers the four operations on /ip/address. This page is the other cut: one realistic task, all three levels, and what changes per transport.

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.

All three are compiled as part of the repository — tik4net.examples/OneTaskEveryTransportExamples.cs.


Part 1 — the three API levels

Low-level API

Raw request words in, raw sentences out. See Low-level API.

using tik4net;

// '?' words are filters. The router answers one !re sentence per matching row, then !done.
var findResponse = connection.CallCommandSync("/interface/print", "?comment=managed-by-tik4net");

// Only !re sentences carry rows — the trailing !done is a sentence too, so select by type rather
// than taking the first one.
var row = findResponse.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
    return;                            // no interface carries that comment

string id = row.GetId();               // the .id word, in the router's '*2' form

// '=' words are name-value. Addressing the row by .id is what makes this an update rather than
// a second search.
var setResponse = connection.CallCommandSync("/interface/set",
    "=.id=" + id,
    "=comment=managed-by-tik4net (checked)");

What comes back. CallCommandSync returns IEnumerable<ITikSentence> — the sentences, unparsed:

Sentence When How to read it
ITikReSentence one per matching row GetId(), GetResponseField("name"), GetResponseFieldOrDefault("comment", null)
ITikDoneSentence last sentence of every reply after add, GetResponseWord() is the new row's .id
ITikTrapSentence the router refused .Messagebinary API only, see below

A field the router did not send is absent, not empty: an interface with no comment has no comment word at all, so GetResponseField("comment") throws and GetResponseFieldOrDefault("comment", null) is the right call. That is also why a filter on a marker comment is a reliable way to find your own rows.

⚠️ 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 TikCommandTrapException instead. Code written against sentence inspection therefore silently stops checking anything when you move it to another transport. If you want error handling that reads the same everywhere, use the ADO.NET-like level or above — they throw on all 11.

CallCommandSync is an extension method (TikRawSentenceExtensions) that casts to ITikRawSentenceConnection; every shipped transport implements it, so the snippet compiles and runs unchanged everywhere.

ADO.NET-like API

Commands, parameters and typed Execute* calls. See ADO.NET-like API.

using tik4net;

// The parameter FORMAT is not stated here — it is decided by the Execute* call.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as the filter ?comment=… .
var findCmd = connection.CreateCommandAndParameters("/interface/print", "comment", "managed-by-tik4net");
var row = findCmd.ExecuteSingleRowOrDefault();     // null when nothing matches
if (row == null)
    return;

string id = row.GetId();

// ExecuteNonQuery writes, so the same helper now emits =comment=… and =.id=… .
var updateCmd = connection.CreateCommandAndParameters("/interface/set",
    "comment", "managed-by-tik4net (checked)",
    TikSpecialProperties.Id, id);
updateCmd.ExecuteNonQuery();                       // throws when the router refuses

What comes back. Every Execute* call decides two things at once — what you get, and how the parameters are formatted on the way out:

Call Returns Nothing found Parameters go out as
ExecuteList() IEnumerable<ITikReSentence> empty list ?name=value (filter)
ExecuteSingleRow() ITikReSentence throws ?name=value (filter)
ExecuteSingleRowOrDefault() ITikReSentence? null ?name=value (filter)
ExecuteScalar() string — the =ret= value (this is how add returns the new .id), or the single word of a one-row reply throws =name=value
ExecuteNonQuery() nothing =name=value (name-value)

That default is why TikSpecialProperties.Id can be passed as an ordinary parameter to the update: under ExecuteNonQuery it is formatted as =.id=*2, which is what set needs. Pass TikCommandParameterFormat.Filter / .NameValue explicitly when you need to override it for one parameter or for the whole command.

A refusal throws TikCommandTrapException (a TikCommandException) on every transport — see Exception handling.

High-level O/R mapper

Typed entities. See High-level API with O/R mapper — this is the recommended level.

using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;

var iface = connection.LoadSingleOrDefault<Interface>(
    connection.CreateParameter("comment", "managed-by-tik4net"));
if (iface == null)
    return;

// No .id is handled by hand: the entity carries it (iface.Id), and Save() addresses the row with it.
iface.Comment = "managed-by-tik4net (checked)";
connection.Save(iface);

What comes back. A typed Interface?null when nothing matched, an exception when the filter matched more than one row. LoadSingle<T> is the same query that throws instead of returning null, and LoadList<T>/LoadAll<T> return IEnumerable<T>.

Three things this level does that the two below it do not:

  • The object is the id. Save(iface) sends =.id= from iface.Id; you never spell it.
  • Only what changed goes out. Save diffs against the snapshot taken when the entity was loaded, so the command on the wire is /interface/set =.id=*2 =comment=… — one field, not the whole row. A Save with nothing changed makes no call at all. See Change tracking.
  • The read asks for the full field set. The entity's metadata carries detail/.cli-stats markers, which matters on the CLI transports — see the next section.

Since properties are nullable (4.0), an unset field on the router reads back as null rather than as "" or 0, and writing null on an update means unset this field where the entity was loaded with a value.


Part 2 — the transports

The three samples above name no transport, and none of them changes. The only line that differs is the one that opens the connection. Apply every option through TikConnectionSetup and one object opens any of them:

var setup = new TikConnectionSetup(host, user, password)
{
    RouterMac = "00:00:00:00:00:00",   // MAC-layer transports only; omit to discover by MNDP
    AllowInvalidCertificate = true,    // TLS transports only; needed for a self-signed certificate
};

using (ITikConnection connection = setup.Create(TikConnectionType.Api))   // ← the only line that varies
{
    // … the three samples, unchanged
}
Api — binary API, TCP 8728
using (var connection = new TikConnectionSetup(host, user, pass).Create(TikConnectionType.Api))

The reference transport. The only family where a refusal arrives as an ITikTrapSentence instead of throwing at the low-level API. Full field set on a bare print. See How to use tik4net.

ApiSsl — binary API over TLS, TCP 8729
var setup = new TikConnectionSetup(host, user, pass) { AllowInvalidCertificate = true };
using (var connection = setup.Create(TikConnectionType.ApiSsl))

Identical to Api in everything the samples touch. AllowInvalidCertificate is false since 4.0, so a router with a self-signed certificate needs it set (or a CertificateValidationCallback). See SSL connection.

Rest / RestSsl — HTTP REST API, TCP 80/443 (RouterOS 7.1+)
using (var connection = new TikConnectionSetup(host, user, pass).Create(TikConnectionType.Rest))

The filter becomes a REST query, the update a POST …/set. Returns the same full field set as the binary API. The service must be enabled on the router (/ip/service, www or www-ssl). See REST connection.

Telnet — plain-text CLI, TCP 23
using (var connection = new TikConnectionSetup(host, user, pass).Create(TikConnectionType.Telnet))

The filter becomes a where clause and the read a :put [/interface print as-value where comment=…]. Note the narrower field set on a bare print — see the table below. See Telnet connection.

Ssh — CLI over an SSH shell, TCP 22
tik4net.Ssh.Tik4NetSsh.Register();     // once at startup — separate tik4net.ssh package
using (var connection = new TikConnectionSetup(host, user, pass).Create(TikConnectionType.Ssh))

Behaves exactly like Telnet; the difference is the carrier. The only transport that needs a registration call, because it ships in its own package (Renci.SshNet dependency). See SSH connection.

MacTelnet — CLI over the MAC layer, UDP 20561
var setup = new TikConnectionSetup(host, user, pass) { RouterMac = "AA:BB:CC:DD:EE:FF" };
using (var connection = setup.Create(TikConnectionType.MacTelnet))

No IP route to the router needed — useful for a device with no address yet. host is still passed but the MAC is what addresses the router; leave RouterMac null to discover it by MNDP. Slower than the IP transports. See MAC-Telnet connection.

WinboxCli — CLI over the WinBox channel, TCP 8291
using (var connection = new TikConnectionSetup(host, user, pass).Create(TikConnectionType.WinboxCli))

Encrypted (EC-SRP5 + AES) with no certificates to manage, on the port WinBox itself uses. CLI family, so the same field-set note as Telnet. See WinBox CLI connection.

WinboxCliMac — WinBox CLI over the MAC layer, UDP 20561
var setup = new TikConnectionSetup(host, user, pass) { RouterMac = "AA:BB:CC:DD:EE:FF" };
using (var connection = setup.Create(TikConnectionType.WinboxCliMac))

WinboxCli's encryption over MacTelnet's carrier — encrypted and routeless at once, and the slowest of the eleven. See WinBox CLI MAC connection.

WinboxNative / WinboxNativeMac — structured WinBox M2, TCP 8291 / UDP 20561
using (var connection = new TikConnectionSetup(host, user, pass).Create(TikConnectionType.WinboxNative))

Not a CLI at all: the call becomes a structured M2 request, the same one the WinBox GUI makes. Fields are addressed by numeric key, so a path or field WinBox has no window for cannot be reached — the connection says so (TikPathNotMappedException) rather than returning wrong data. Rows also carry a few native-only fields the API does not report. See WinBox Native connection.

What actually differs

Measured on RouterOS 7.24 by running the three samples above over all eleven transports — 33 of 33 combinations found the row and wrote the comment through, 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
Refusal at the low-level API ITikTrapSentence throws throws throws
Refusal at the other two levels throws throws throws throws

The field-set row is the one that catches people. A bare print on a CLI transport becomes print as-value, which returns the CLI's summary columns — for a commented /interface row that is 8 fields against the binary API's 28. mtu, default-name, last-link-up-time and the byte counters are simply not there. Add detail to get most of them back (the byte counters need the stats query on top):

var cmd = connection.CreateCommandAndParameters("/interface/print", "comment", "managed-by-tik4net");
cmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);

The O/R mapper already does this for you — an entity declares IncludeDetails / IncludeCliStats and the mapper adds both markers to every load, so a typed Interface is complete on all 11 transports. It is only the two lower levels, where you write the command yourself, that see the narrower set.

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