-
Notifications
You must be signed in to change notification settings - Fork 98
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.
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.
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 |
.Message — binary 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 onApi/ApiSslonly. They speak the binary protocol, where a refusal really is a!trapsentence 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!trapto hand back, and throwsTikCommandTrapExceptioninstead. 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.
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 refusesWhat 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.
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=fromiface.Id; you never spell it. -
Only what changed goes out.
Savediffs 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. ASavewith 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-statsmarkers, 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.
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.
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.
- CRUD examples for all APIs — create/read/update/delete per API level
-
Connection types & capabilities — the capability matrix and
TikConnectionSetup -
Command translation on non-API transports — what each
Execute*becomes on the wire -
Change tracking — why
Savesends one field -
Exception handling — the exception hierarchy behind
TikCommandTrapException