-
Notifications
You must be signed in to change notification settings - Fork 97
Writing your own transport
tik4net ships eleven transports, but the connection contract is open: a transport can be written in your own assembly, and everything above it — the ADO.NET-like commands and the O/R mapper — works on it without changes. This page is for someone doing that: a transport for a channel tik4net does not speak (a serial console, a jump host, a proxy of your own), or a wrapper around an existing one.
If what you want is a connection that answers from memory so you can test your code without a router,
you do not need this page — use TikFakeConnection from tik4net.testing, see
Unit testing without a router.
Derive from TikCommandConnectionBase (namespace tik4net.Connection). It implements the whole
ITikConnection surface — command factory, timeouts, diagnostics events — and hands the real work to hooks
you override:
| You implement | Called for |
|---|---|
Open ×2, OpenAsync ×2, Close
|
the connection's lifecycle |
RunPrint(TikCommandDescriptor) → rows |
every read: ExecuteList, ExecuteSingleRow, ExecuteScalar on a non-add command, and so every Load* of the mapper |
RunAdd(TikCommandDescriptor) → new .id
|
ExecuteScalar on an add command, and Save of a new entity |
RunNonQuery(TikCommandDescriptor) |
ExecuteNonQuery: set, remove, move, enable, unset, actions… — Save of an existing entity, Delete, Move
|
optional RunPrintAsync, RunAddAsync, RunNonQueryAsync
|
the Execute*Async / *Async mapper calls |
optional RunRawText (+ RunRawTextAsync) |
CreateRawCommand — a command in your transport's own language |
The optional hooks throw by default. That is deliberate: a transport that cannot genuinely await its I/O
should not pretend to, so the default is "not supported" rather than a Task.Run wrapper around the
synchronous hook.
This one keeps its "router" in memory, which is enough to show every hook — and the mapper runs on it
unchanged. Demo.Run at the end uses it the way any tik4net connection is used:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using tik4net;
using tik4net.Connection;
using tik4net.Objects;
using tik4net.Objects.Ip;
public sealed class InMemoryConnection : TikCommandConnectionBase
{
// menu path ("/ip/address") → its rows; each row is RouterOS field name → value
private readonly Dictionary<string, List<Dictionary<string, string>>> _menus =
new Dictionary<string, List<Dictionary<string, string>>>();
private int _nextId = 1;
// Only what this transport really does. No AsyncCommands (the Run*Async hooks are not overridden),
// no RawCommand (it has no command language of its own), no Listen/Streaming.
public override TikConnectionCapability Capabilities => TikConnectionCapability.Crud;
public override void Open(string host, string user, string password) => Open(host, 0, user, password);
public override void Open(string host, int port, string user, string password) => SetOpened();
public override Task OpenAsync(string host, string user, string password,
CancellationToken cancellationToken = default)
=> OpenAsync(host, 0, user, password, cancellationToken);
public override Task OpenAsync(string host, int port, string user, string password,
CancellationToken cancellationToken = default)
{
SetOpened();
return Task.CompletedTask;
}
public override void Close() => SetClosed();
protected override IList<TikRecordSentence> RunPrint(TikCommandDescriptor descriptor)
{
EnsureOpened();
// Filters arrive with ParameterFormat.Filter. This sample understands plain equality only; a real
// transport must translate every query form it accepts, or refuse it — never ignore a filter.
var filters = descriptor.Parameters.Where(p => p.ParameterFormat == TikCommandParameterFormat.Filter).ToList();
return Rows(descriptor.CommandText)
.Where(row => filters.All(f => row.TryGetValue(f.Name, out var v) && v == f.Value))
.Select(row => new TikRecordSentence(new Dictionary<string, string>(row)))
.ToList();
}
protected override string RunAdd(TikCommandDescriptor descriptor)
{
EnsureOpened();
string id = "*" + (_nextId++).ToString("X");
var row = new Dictionary<string, string> { [TikSpecialProperties.Id] = id };
foreach (var p in descriptor.Parameters)
row[p.Name] = p.Value ?? "";
Rows(descriptor.CommandText).Add(row);
return id;
}
protected override void RunNonQuery(TikCommandDescriptor descriptor)
{
EnsureOpened();
string verb = descriptor.CommandText.Substring(descriptor.CommandText.LastIndexOf('/') + 1);
string? id = descriptor.Parameters.FirstOrDefault(p => p.Name == TikSpecialProperties.Id)?.Value;
var rows = Rows(descriptor.CommandText);
var row = rows.FirstOrDefault(r => r[TikSpecialProperties.Id] == id)
?? throw new TikNoSuchItemException(CreateCommand(descriptor.CommandText));
switch (verb)
{
case "set":
foreach (var p in descriptor.Parameters.Where(p => p.Name != TikSpecialProperties.Id))
row[p.Name] = p.Value ?? "";
break;
case "remove":
rows.Remove(row);
break;
default:
throw new NotSupportedException($"'{verb}' is not implemented by this transport.");
}
}
// "/ip/address/print" → the rows of "/ip/address"
private List<Dictionary<string, string>> Rows(string commandText)
{
string menu = commandText.Substring(0, commandText.LastIndexOf('/'));
if (!_menus.TryGetValue(menu, out var rows))
_menus[menu] = rows = new List<Dictionary<string, string>>();
return rows;
}
}
public static class InMemoryConnectionSetupExtensions
{
// Reads like a built-in transport: setup.CreateInMemoryConnection()
public static InMemoryConnection CreateInMemoryConnection(this TikConnectionSetup setup)
{
var connection = new InMemoryConnection();
setup.ApplyTo(connection); // timeouts, encoding, … exactly as the built-in transports get them
connection.Open(setup.Address.Host!, setup.User, setup.Password);
return connection;
}
}
public static class Demo
{
// Save, LoadAll and Delete all go through the three hooks above.
public static void Run()
{
var setup = new TikConnectionSetup("192.0.2.1", "admin", "");
using var connection = setup.CreateInMemoryConnection();
var address = new IpAddress { Address = "192.0.2.10/24", Interface = "ether1" };
connection.Save(address); // RunAdd — address.Id is now set
address.Comment = "lab";
connection.Save(address); // RunNonQuery: /ip/address/set .id=*1 comment=lab
var all = connection.LoadAll<IpAddress>(); // RunPrint
connection.Delete(address); // RunNonQuery: /ip/address/remove
}
}Everything arrives in one TikCommandDescriptor:
-
CommandText— the path and verb in API form,/ip/address/print. Multi-line command text written by a caller has already been split: you get the path, and its?/=rows as parameters. -
Parameters—Name,Value,ParameterFormat. On a read, every parameter the caller did not format explicitly has already been resolved (toFilter, or to the command's own default format), so aFilterparameter is a query and anything else is an argument. On a write, parameters are arguments to the verb (DefaultandNameValuemean the same thing there);set/remove/movecarry the row's.idas a parameter namedTikSpecialProperties.Id. -
IsRaw/WrapAsValue— only forCreateRawCommand; see Capabilities below.
Two parameters are not about data. .proplist asks for a subset of fields — honour it or ignore it,
returning more fields than asked for is harmless. .tag belongs to the binary API's multiplexing and can be
ignored by any other transport.
Filters are the part to be careful with. A transport that drops a filter it cannot translate turns
LoadSingle into "the first row of the whole table" — a wrong answer that looks right. Translate every
query form you accept, and throw for the rest.
-
RunPrintreturns oneTikRecordSentenceper row, built from a dictionary keyed by RouterOS field names (address,.id, …) with the values as the router writes them. The mapper converts from there. The dictionary is held by reference, so hand over a fresh one per row. -
RunAddreturns the new row's.id. -
Errors. Use the library's exception types, so callers' error handling does not have to know which
transport they are on (see Exception handling):
- a row that does not exist →
TikNoSuchItemException, as the sample does; - the router refusing a command →
TikCommandTrapException, which takes the command (CreateCommand(descriptor.CommandText)builds one) and anITikTrapSentence— a small class of your own carrying the router's message; - the connection torn down while a command runs → the base's
ClosedWhileRunning(inner); - a command on a closed connection → call
EnsureOpened()first, as the sample does.
- a row that does not exist →
Override Capabilities and declare only what is true — callers on a connection picked at run time ask
Supports(...) before they call, and a flag that promises too much turns their check into a runtime failure
further down. The base answers Crud alone. What each flag commits you to:
-
AsyncCommands— override theRun*Asynchooks and genuinely await your I/O. Declaring it with the defaults in place makes every async call throw. -
RawCommand— overrideRunRawText(and handleIsRawinRunPrint), soCreateRawCommandcan send a command in your transport's own language verbatim. ImplementITikRawSentenceConnectiontoo if you want the low-levelCallCommandSync. -
SafeMode— implementITikSafeModeConnection; the base keepsSafeModeHeldfor you.
The full list and what every built-in transport declares is in Connection types and capabilities.
The base offers a lock — the protected _cmdLock semaphore — but does not take it for you: the hooks run
without it held. Decide what your channel needs and document it. A channel that carries one conversation at
a time (a terminal, a serial line) must take it around every command, or two threads will read each other's
answers; one where each command is independent (like HTTP) does not need it. The built-in transports make
each of these choices — see Threading and concurrency.
Create it with new, pass it to TikConnectionSetup.ApplyTo, then open it — what
CreateInMemoryConnection in the sample does.
ApplyTo is public for exactly this: it applies the same timeouts and encoding the built-in transports
get, and the transport-specific options (AllowInvalidCertificate, RouterMac, CancellationMode)
reach your connection if it implements the interface that declares them (ITikTlsConnection,
ITikMacLayerConnection, ITikCancellationModeConnection).
To make it read like a built-in transport, add a Create…Connection extension method on
TikConnectionSetup in your own namespace — InMemoryConnectionSetupExtensions at the end of the sample
above. That is how the tik4net.ssh package offers CreateSshConnection.
ConnectionFactory.RegisterConnectionFactory is not for a new transport. It registers a factory for a
TikConnectionType, and that enum is closed: every value except Ssh is implemented by tik4net itself, and
registering one of those throws ArgumentException. The method exists so that the separate tik4net.ssh package can fill the
Ssh slot, which makes TikConnectionSetup.Create(TikConnectionType.Ssh) work. A transport of your own
has no TikConnectionType value and is created directly, as above.
OnWriteRow / OnReadRow / DebugEnabled work on your transport if you call the base's FireWriteRow
and FireReadRow with what you send and receive. RowTracingEnabled says whether anyone is listening, so
you can skip building an expensive trace line when nobody is, and DiagnosticPrefix names your channel
in the debug output. See Communication debugging.
Start here
- Getting started — first project
- Which API level?
- One task, every transport & level — 29 runnable programs
- CRUD examples, all levels
- Upgrading from 3.x · with an AI agent
- Upgrading from 4.x to 5.0 · with an AI agent — unreleased
API levels
-
High-level — O/R mapper
- Reading data
- CRUD · async CRUD
- Advanced — streaming, ordering
- Change tracking
- List merging
- ADO.NET-like — commands, parameters
- Low-level — raw sentences
- VB.NET example
Entities
- Entity reference — all 171 menus
- How the mapping works
- TikValue — what a loaded property holds, and why
- Custom entities
- Value types · helpers
- Scaffolding tools
Transports
- Types & capabilities — the matrix
- Threading & concurrency — sharing one connection
- API-SSL · login versions
- REST
- Telnet · SSH
- MAC-Telnet
- RoMON — through a neighbouring router
- WinBox CLI · over MAC
- WinBox native · over MAC
- Command translation
- MNDP discovery
- Writing your own transport
Safety & diagnostics
- Safe Mode — rollback protection
- Exception handling
- Communication debugging
- Testing without a router
Project
- RouterOS versions — what is tested, and how versions differ
- MCP server
- History / changelog