Repository navigation
TikField
Every property of a built-in entity, except .id, is a TikField<T>: a value that also says whether the router
printed the field at all. Read this page to understand what a loaded entity holds, what a save sends, and why the
property types look the way they do.
🆕 New in 5.0. In 4.x entity properties were plain
string?,bool?,longand enums. Moving code over is covered in Upgrading from 4.x to 5.0.
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Ip.Firewall;
using ITikConnection connection = new TikConnectionSetup("192.168.88.1", "admin", "").Create(TikConnectionType.Api);
foreach (var rule in connection.LoadAll<FirewallMangle>())
{
string? comment = rule.Comment.Value; // null when the router printed no comment
bool disabled = rule.Disabled.GetValueOrDefault(); // false when absent, as on Nullable<T>
if (rule.Action == FirewallMangle.ActionType.Jump) // compares a value the router printed
Console.WriteLine($"{rule.Chain} -> {rule.JumpTarget} ({comment ?? "no comment"}, disabled: {disabled})");
if (rule.Action.State == TikFieldState.Unparsed) // a word this tik4net version does not know
Console.WriteLine($"unknown action '{rule.Action.RawValue}'");
}A RouterOS row carries only the fields the router chose to print, and that choice varies:
- by RouterOS version. A field one version has, another does not. Some fields were renamed, and enum vocabularies grow and shrink between versions.
-
by the row itself. A mangle
jumprule has nopassthrough. A static route prints noconnectflag. Many fields are printed only once they hold a value. -
by transport. A terminal print leaves out some fields the binary API returns, and a
.proplistload asks for a subset.
A plain property has no way to say "the router did not print this", so something else had to stand in:
- a
boolreadfalse, a number0, an enum its first member; - a
stringreadnullor""; - a property with a declared
DefaultValueread that default.
Each of those looks exactly like a real value. An interface that was up could read Running = false, and nothing
on the entity said it was invented.
DefaultValue then carried three jobs at once: what a missing field read as, what an add left out when a value
equalled it, and what change tracking compared against. A default that differed from the router's own, which
happens between RouterOS versions, broke all three. An explicit value you assigned could be silently dropped from
an add, because it matched the declared default, and the router then applied a different one.
And a word the property type could not hold, such as a new enum member in a newer RouterOS or a number in an unfamiliar format, failed the load of the whole menu: every row and every property, over one field.
TikField<T> removes the guessing. The state travels with the value, so:
- a read never fails because of what another RouterOS version prints;
- a read never invents a value. A field the row lacks is absent, and you pick the fallback where you use it;
- a save sends what you assigned or changed, and nothing else;
- a word the library cannot read is kept, not lost. A save writes it back unchanged.
State |
Meaning | .Value |
|---|---|---|
Absent |
the row did not carry the field, or the entity was never loaded (the default of the struct) | null |
Present |
printed and read, or assigned by you | the value (may be null if you assigned null) |
Unparsed |
printed, but T cannot hold the word: an enum member this version does not know, a number in another format |
throws TikUnparsedValueException; RawValue holds the router's word |
T is always the nullable form: TikField<string?>, TikField<bool?>, TikField<long?>,
TikField<FirewallMangle.ActionType?>, TikField<TikDuration?>. That is what lets = null compile on every
property.
| You want | Write |
|---|---|
the value, null when absent |
rule.Comment.Value (throws on an unparsed value rather than invent one) |
| the value, or the type's default |
rule.Disabled.GetValueOrDefault(): false / 0 for a value type, null for a reference type, as on Nullable<T>. Never throws |
| the value, or your own fallback |
email.Port.Value ?? 25, or ValueOrDefault(25)
|
| to branch on having a value | rule.Comment.TryGetValue(out var text) |
| to compare |
rule.Action == FirewallMangle.ActionType.Jump: true only for a present, equal value |
| "has no value" |
rule.Comment == null: absent, or an assigned null. An unparsed value is never null
|
| the router's spelling |
rule.Action.ToString(): jump, not Jump; "" when absent |
| why there is no value |
rule.Action.State, and rule.Action.RawValue for an unparsed word |
<, >, <=, >= compare like a nullable: false without a value. A sort puts absent first, then unparsed, then
present values in order.
Flags the router prints only when set. A flag that is off is often simply left out: a static route has no
connect, and a presence flag such as /routing/table fib exists only when it is on. Such a flag reads absent,
not false, so test it with == true or GetValueOrDefault(); == false never matches it.
A set of words is a list, not a [Flags] enum: connection-state is a TikField<TikValueList<FirewallConnectionState>?>
(see Value lists). A word the enum has no member for is an item of its own, and a
[Flags] enum as a TikField's value is refused when the entity is first used.
Assignment is implicit:
var rule = connection.LoadAll<FirewallMangle>().First();
rule.Comment = "managed by my tool";
rule.Disabled = true;
rule.Comment = null; // unsets the field on saveA literal for a duration or rate property needs its type named, because C# applies one user-defined conversion,
not two: dhcpServer.LeaseTime = (TikDuration)"1d", queue.MaxLimit = (TikRatePair)"1M/2M".
What a save sends:
- An add sends every property you assigned, including a value equal to the router's default. Nothing else is sent: not a "mandatory" field, not a type default. A field the router needs on add is one you assign.
-
An update of a loaded entity sends what changed since the load, as the router spells it. A loaded value you
set to
nullis unset. AnUnparsedvalue you did not touch is left as it is, so a word the library cannot read survives a save. - A singleton saved without loading it first sends only what you assigned.
DefaultValue on an entity property documents the router's default. It is never read into a property and never
decides what is sent.
A TikField<T> says whether the router printed the field, and holds one TikValue<T>: the value, or the router's
word when T cannot hold it, with its own !. You meet TikValue<T> when you write something more than a plain
value. Both forms convert to the property's TikField<T>:
TikValue<string?> outside = TikValue<string?>.Not("10.0.0.0/8"); // the router's !10.0.0.0/8
Console.WriteLine(outside.IsNegated); // True
Console.WriteLine(outside); // !10.0.0.0/8
var newer = TikValue<FirewallMangle.ActionType?>.FromWire("some-new-action"); // a word this version's enum lacks
Console.WriteLine(newer.IsWord); // True; .Value would throw, .RawValue has the word
var rule = new FirewallMangle { Action = newer }; // reads back as Unparsed, saved as written| Member | Meaning |
|---|---|
Value |
the value; throws TikUnparsedValueException when this is the router's word |
IsWord, RawValue
|
the router's word that T cannot hold, and the word itself |
IsNegated, WithoutNegation()
|
the !, and the value without it |
Not(value), FromWire(word)
|
a negated value; the router's own word, sent as written |
A field that holds several values — ports, connection states, address types, DNS servers — is a
TikField<TikValueList<T>?>. The list is immutable: With and Without return a new one, so assign the result
back. Each item is a TikValue<T>, so an item can be a word T lacks without the rest of the list losing its type. A port
list holds TikPortRange items and a number list (vlan-ids, netwatch http-codes) TikNumberRange items, each a
single value (22, an int converts) or a range (new TikPortRange(1000, 2000), read and written 1000-2000).
TikField<TikValueList<TikPortRange>?> dstPort = new TikValueList<TikPortRange>(22, 8291, new TikPortRange(1000, 2000));
dstPort = dstPort.Value!.With(443); // assign the new list back
Console.WriteLine(dstPort); // 22,8291,1000-2000,443
TikField<TikValueList<TikPortRange>?> notThese =
TikValue<TikValueList<TikPortRange>?>.Not(new TikValueList<TikPortRange>(22, 8291));
Console.WriteLine(notThese); // !22,8291 — the whole list negated
var topics = new TikValueList<string>("info", TikValue<string>.Not("debug"));
Console.WriteLine(topics); // info,!debug — one member negated
var flags = TikValueList<FirewallTcpFlag>.Parse("syn,!ack"); // the router's spellingParse reads a list the way the router writes it — items separated by ,, a ! in front of an item negating that
item — and refuses an item that is not a T; a word from another RouterOS version goes in as TikValue<T>.FromWire.
A string does not convert to a list implicitly: C# applies one user-defined conversion per assignment, and the
property's is the one from the list. A ! on the whole list is not part of the list, so Parse refuses !,syn;
write it as TikValue<TikValueList<T>?>.Not(list).
-
Two lists are equal when they hold the same items the same number of times, in any order. The router prints
some lists in an order of its own (
ack,!syn,finreads backfin,ack,!syn), and that must not look like a change. A save sends the items in the list's order, and a port list keeps it on the router. -
Which
!a field takes is declared on the entity:Negatablefor the whole list,NegatableMembersfor each member. A!the field does not take is refused before anything is sent, and so is a negated empty list. -
tcp-flagsover the API, REST and the CLI: a textsetreplaces only the half it names, plain or negated members. An update whose new value lacks a half the row holds is refused before sending, with the reason; name at least one member of each kind, or write over WinBox native (StructuredWrites), which writes both halves exactly.
A firewall-style matcher can be negated: src-address=!10.0.0.0/8 matches every address outside that network, and
connection-state=!established,related every state but those two. The ! is not part of the value. It reads as
IsNegated, the value itself parses as usual, and TikValue<T>.Not(value) writes one:
var rule = connection.LoadAll<FirewallFilter>().First(r => r.SrcAddress.IsNegated);
Console.WriteLine(rule.SrcAddress.Value); // 10.0.0.0/8
Console.WriteLine(rule.SrcAddress); // !10.0.0.0/8
rule.SrcAddress = TikValue<string?>.Not("192.168.0.0/16"); // saved as src-address=!192.168.0.0/16
rule.DstPort = rule.DstPort.WithoutNegation(); // the matcher without its '!'-
A negated value is never equal to the plain one.
rule.SrcAddress == "10.0.0.0/8"is false for!10.0.0.0/8, and<,>are false for a negated number. Compare withTikValue<string?>.Not("10.0.0.0/8"), or testIsNegatedand.Value. -
The
!negates the whole value, a list included:dst-port=!22,8291is "neither 22 nor 8291". A field whose members are negated one by one, such astcp-flags=syn,!ack, keeps its!s in the value. -
Only the matchers RouterOS negates read this way. They are marked
Negatablein the shipped entities: the firewall filter, NAT, mangle and raw rules, bridge filter and NAT rules, hotspot walled-garden entries and web-proxy access rules. Anywhere else a leading!is text: a comment may start with one. Writing a negated value to a property that is not negatable throws rather than drop the!.
A load does not fail on a value it could not read. Where your code must not act on such a value, a firewall action or a queue kind, say so after loading:
var rules = connection.LoadAll<FirewallMangle>().EnsureAllStrict(); // throws, naming every Unparsed field
var identity = connection.LoadSingle<tik4net.Objects.System.SystemIdentity>().EnsureStrict();
foreach (var item in identity.GetValueReport()) // every field: state and raw word
Console.WriteLine(item);TikStrictness.Absent also refuses a field the row did not carry, except the ones you name as allowed to be
missing; TikStrictness.UnknownListItems refuses a list holding an item its type cannot hold.
To see drift wherever it happens, not only where you check, attach a sink to the connection. It gets a
TikEntityReadReport for every entity read that found something, and nothing for a clean one:
connection.SetEntityDiagnostics(report => Console.WriteLine(report));
// IpRoute (/ip/route/print, 12 rows): absent from every row: routing-table; unmapped: routing-vrf-
UnparsedFields: values the property's type could not hold, with the router's word and how many rows had one. -
FieldsAbsentFromEveryRow: mapped fields no row carried. A field this version does not have, or has renamed, or prints only for some rows and none of these. Fields your own.proplistdid not ask for are left out. -
UnmappedFields: fields the router sent that no property maps. A renamed field appears here under its new name, next to the old one inFieldsAbsentFromEveryRow. Empty on WinBox native, whose windows carry fields the API never prints.
A row delivered by a callback or a listen is reported on its own (IsComplete is false), so it has no
"absent from every row". The sink runs on the loading thread before the load returns; an exception it throws is
ignored. One sink per connection; ClearEntityDiagnostics() removes it.
TikListMerge compares TikField<T> fields by the form the router would be sent. "No value",
absent or an assigned null, is one state there, as it is for == null.
Two field rules cover the cases where absence means something:
-
.Field(x => x.F, (expected, current) => expected.IfAbsent(current))keeps the router's value where your expected row lacks the field, instead of unsetting it. -
.Field(x => x.F, (expected, current) => expected.IfPrintedIn(current))leaves a row alone where the router does not print the field for that kind of row:passthroughon a manglejump, for example. Without it, an expected value the router never prints makes the rows differ on every run.
-
No implicit conversion back to
T.Foo(rule.Comment)whereFootakes astringdoes not compile; writerule.Comment.Value. -
Comparisons through
objectnever match.object.Equals(rule.Action, FirewallMangle.ActionType.Jump),CollectionAssert.AreEqual, or a plain value in aHashSet<object>compare a wrapper (aTikFieldor aTikValue) with a value. Compare.Value. MSTest's genericAssert.AreEqual(FirewallMangle.ActionType.Jump, rule.Action)is fine, sinceTis inferred as theTikField. The compiler warns about the direct forms —object.Equals,Assert.AreEqual(object, object), a plain value'sEquals(object)— as TIK001, an analyzer in the tik4net package, with a fix to.Value. A collection or a FluentAssertions chain it cannot see. -
Assert.IsNull(entity.Prop)checks nothing. It takesobject, the struct is boxed and never null. WriteAssert.IsTrue(entity.Prop == null). -
Valuethrows on an unparsed value,GetValueOrDefault()does not. Pick the one that fits: the throwing form for a value you act on, the tolerant one for display.
On .NET 8 and later a TikField<T> serializes with System.Text.Json as its value: an enum as the router's word,
TikDuration and the other value types as the router spells them. Absent serializes as null, unparsed as
{"$raw":"word"}, a negated value as {"$not":value}, and all three read back the same way; a null reads back absent, so a deserialized entity never
unsets a field. Newtonsoft.Json and the netstandard2.0 build have no converter: serialize .Value.
A custom entity may use TikField<T> properties, plain ones, or both. A plain property keeps the older rules: a
missing field reads as the property's DefaultValue or its type's default, and an unknown enum word needs an
[TikEnumUnknown] member. IsMandatory and UnsetOnDefault apply to plain properties only; both are refused on a
TikField<T> property. Negatable = true on [TikProperty] marks a matcher RouterOS negates with a !; it needs a
TikField<T> property. See Custom entities.
-
Entity value types:
TikDuration,TikDataRate,TikRatePair, the types inside the wrapper. - How the mapping works and Change tracking.
- TikListMerge.
- Upgrading from 4.x to 5.0.
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
- TikField — 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