-
Notifications
You must be signed in to change notification settings - Fork 97
Upgrading from 4.x to 5.0
🔷 5.0 is not released yet. This page describes
master, and it grows as the 5.0 changes land. See History for the full list.
The short version: every entity property is now a TikValue<T?>. Fix the compile errors that raises, then review
the places that still compile but mean something different.
Package IDs, namespaces, class names and the connection API are all the same as in 4.0. What changed is the type of every mapped entity property: it now says whether the router printed the field. Most of the upgrade is mechanical, and the compiler finds all of it. The part it cannot find is code that compiles but changed meaning. That part matters most if you merge or diff entity lists.
Want an AI coding agent to do the upgrade? There is a companion page, Upgrading from 4.x to 5.0 with an AI agent, with a ready-to-paste prompt that walks Claude Code / Copilot / Cursor / Aider through every step on this page. Commit first, then review its diff and its list of behaviour-review findings — and run a merge in simulate mode against a real router, because a changed merge result is what no agent or compiler can see.
Change the tik4net reference to 5.0 (and tik4net.testing if you use it). Nothing else in the package layout
changed: the O/R mapper still ships inside tik4net. Update every leg of a conditional reference too (a CI
package reference beside a local project reference, say): a leg left on 4.x no longer compiles against 5.0 code.
Documentation and agent instructions in your project that prescribe 4.x idioms (x.Disabled ?? false) need the
same update.
Every property of a built-in entity except .id is a TikValue<T?>: string? became TikValue<string?>, bool?
became TikValue<bool?>, an enum TikValue<TheEnum?>, and so on. A TikValue<T> knows what the router sent:
State |
Meaning | .Value |
|---|---|---|
Absent |
the router did not print the field | null |
Present |
a value | the value |
Unparsed |
printed, but T cannot hold the word (say, an enum member this version does not know) |
throws TikUnparsedValueException; the word is in RawValue
|
var rule = connection.LoadAll<FirewallMangle>().First();
string? comment = rule.Comment.Value; // null when the router printed no comment
bool disabled = rule.Disabled.GetValueOrDefault(); // like Nullable<T>: false when absent or unparsed
string chain = rule.Chain.Value ?? ""; // your own fallback
bool isJump = rule.Action == FirewallMangle.ActionType.Jump; // compares a present value
bool noComment = rule.Comment == null; // "has no value": absent, or null
rule.Comment = "managed"; // assignment stays implicit
rule.Comment = null; // clears the field on saveToString() is the router's own spelling (jump, not Jump), and "" when absent. The full model, including
TryGetValue, [Flags] values and strictness, is in
Custom entities.
Two consequences reach beyond the property type:
-
A field the router did not print reads absent, not as the type's default and not as the property's
DefaultValue.DefaultValueon the built-in entities documents the router's default and does nothing else. - An add sends exactly what you assigned. Nothing is sent because it is "mandatory" or because it is the type's default.
Each error has one mechanical rewrite. Build again after each round: a lambda with a binding error hides the errors behind it, and a project whose dependency failed is not compiled at all, so a second, smaller round is normal.
| Error | 4.x code | 5.0 code |
|---|---|---|
| CS9135 |
switch (rule.Action) { case ActionType.Jump: … }, x switch { … }
|
switch (rule.Action.Value): throws on an unparsed value, usually right for an action; otherwise check State first |
| CS1503, CS0029 | a property passed as or assigned to string (Foo(rule.Comment), string s = r.Name) |
.Value (may be null; keep the null handling the code already has) |
| CS1503, CS0029 | a property passed as or assigned to a non-nullable long, int, bool (a 4.x non-nullable property such as torch Tx) |
.GetValueOrDefault(): absent reads 0/false, as in 4.x. .Value is long? and fails again |
| CS1503 |
string.Equals(r.Chain, x, …), string.IsNullOrEmpty(r.Comment)
|
r.Chain.Value, r.Comment.Value
|
| CS1929, CS1061, CS0411 | a string member on the property: r.Comment.StartsWith(…), .Replace(…), .Trim()
|
r.Comment.Value?.StartsWith(…) == true, or (r.Comment.Value ?? "") where 4.x relied on a non-null string |
| CS0019 |
x.Disabled ?? false, r.Comment ?? ""
|
x.Disabled.GetValueOrDefault(), r.Comment.Value ?? "" (ValueOrDefault("") returns string?, a nullable warning in nullable-enabled code) |
| CS0019 | list.FirstOrDefault()?.Interface ?? throw … |
list.FirstOrDefault()?.Interface.Value ?? throw … |
| CS0023 |
!a.Invalid (a 4.x non-nullable bool) |
!a.Invalid.GetValueOrDefault() |
| CS0029 | if (addr.Disabled) |
addr.Disabled == true (absent and unparsed are not true) |
| CS0029, CS1662 |
.WithKey(r => r.Name) and other Func<T, string> lambdas |
r => r.Name.ToString() |
| CS0029 |
.Select(r => r.Name).ToArray() into string[]
|
.Select(r => r.Name.Value) |
| CS0029 | a literal assigned to a rate or duration: MaxLimit = "1M/2M", e.Timeout = "none"
|
(TikRatePair)"1M/2M", (TikDuration)"none": C# applies only one user-defined conversion |
| CS1061 | a member of the inner value: q.LimitAt.Token, x.Rate.HasValue
|
q.LimitAt.Value?.Token, x.Rate.Value.HasValue
|
| CS1061 | rule.ConnectionState.HasFlag(F.Established) |
rule.ConnectionState.GetValueOrDefault().HasFlag(…) |
| CS0023 | x.Time?.ToString() |
x.Time.Value?.ToString() |
| CS0117 | SomeEnum.Unknown |
prop.State == TikValueState.Unparsed, and prop.RawValue for the word |
CS1501 on GetValueOrDefault()
|
x.Rx.GetValueOrDefault() in a file without using tik4net.Objects;
|
add the using. The message names .NET's dictionary overload, the only one in scope |
| CS1503 on a conditional argument | Foo(flag ? t.Rx : t.Tx) |
flag ? t.Rx.GetValueOrDefault() : t.Tx.GetValueOrDefault() |
x.Disabled.GetValueOrDefault(), written for a 4.x bool?, compiles unchanged and keeps its meaning.
None of these produce a compiler error. Search for each one, in production code and in tests.
A TikValue<T> never equals a plain T through object: object.Equals, CollectionAssert.AreEqual /
AreEquivalent, a HashSet<object>, FluentAssertions' .Should().Be(…). Compare .Value. MSTest's generic
Assert.AreEqual(FirewallMangle.ActionType.Jump, rule.Action) stays correct (T is inferred as the TikValue).
Assert.IsNull(entity.Prop) and Assert.IsNotNull(entity.Prop) take object: the struct is boxed and never
null, so IsNull always fails and IsNotNull always passes, checking nothing. Write
Assert.IsTrue(entity.Prop == null).
A flag the router prints only when it is set reads absent otherwise, and absent is not false: x.Dynamic == false
matches no such row. Write !x.Dynamic.GetValueOrDefault().
$"{rule.Action}", string.Format("{0}", rule.Action) and "x" + rule.Comment print the router's word (jump,
not Jump) and "" for an absent value. That is harmless where both sides of a comparison are formatted the same
way, such as a merge key. It matters where the text is stored, or compared with text made elsewhere.
In 4.x a field the router did not print read as the type's default or the property's DefaultValue; now it reads
absent. ToolEmail.Port on a router that prints no port read 25 and is now absent. Where the default is what
your code means, say so:
var email = connection.LoadSingle<ToolEmail>();
int port = email.Port.Value ?? 25;This is the one that can rewrite a router. When you build entities in memory and compare them with rows loaded
from the router (CreateMerge, SaveListDifferences, your own diffing), check each assigned property: does the
router print that field for this kind of row?
A mangle jump, return or accept rule has no passthrough, so the router prints none. In 4.x an expected
Passthrough = true compared equal, because the missing field read as its default. In 5.0 the loaded row has it
absent, so the rows differ and every run updates the row. Remove such assignments, or give the field a merge
rule that leaves the rows it does not apply to alone:
var expected = new List<FirewallMangle>();
var current = connection.LoadAll<FirewallMangle>();
connection.CreateMerge(expected, current)
.WithKey(r => r.Comment.ToString())
.Field(r => r.Chain)
.Field(r => r.Action)
.Field(r => r.Passthrough, (e, c) => e.IfPrintedIn(c))
.Simulate(out int inserts, out int updates, out int deletes);Use IfPrintedIn only for fields that do not apply to some rows; on an optional field that is merely unset it
would never write the value. A milder, one-time variant needs no fix. Many fields are printed only once they hold a
value, so an expected Log = false against a row where log was never set is written once, and later runs are
equal. Run the merge with Simulate against unchanged data first: it should report no updates.
An expected row that leaves a field out still unsets it on the router, as in 4.x. Where the expected rows come from
a source that may lack the field, keep the router's value with (e, c) => e.IfAbsent(c). See
TikListMerge.
4.x sent some fields you did not assign: a field marked IsMandatory, and on a singleton saved without being
loaded, every non-nullable field (tls=no on /tool/e-mail). 5.0 sends what you assigned. An add that now fails
with missing … names the field to assign.
A fake router that prints every field, "" for the empty ones, now loads Present(""), which is not equal to an
absent or null expected value. Make the fake leave out fields with no value, as RouterOS does. tik4net.testing's
TikFakeConnection.WithEntities does.
.Value throws on an unparsed value; GetValueOrDefault() treats it as "no value". Where your code must not act on
a value it could not read (a firewall action, a queue kind), check after loading:
var rules = connection.LoadAll<FirewallMangle>().EnsureAllStrict();EnsureAllStrict throws, naming the fields; GetValueReport() lists every field's state.
The package does not touch your [TikEntity] classes. A plain string? / bool? / enum property keeps the 4.x
behaviour: a missing field reads as its DefaultValue. Converting them to TikValue<T?> is optional; it gives them
the same absent / present / unparsed states as the built-in entities. When you convert one:
- Every
[TikProperty]except.idbecomesTikValue<T?>, in the nullable form (TikValue<int>is refused when the entity is first used). In a project without nullable reference types writeTikValue<string>. - Remove
IsMandatoryandUnsetOnDefaultfrom those properties: both are refused on aTikValue<T>property, and what they did is now the default behaviour. -
Delete constructor assignments of mapped properties. On a
TikValueproperty such a value isPresent: it is sent on every add, and every loaded entity, built through the same constructor, reads it for a field the router did not print. Keep the default inDefaultValue. - An enum used only by converted properties no longer needs its
[TikEnumUnknown]member: an unknown word readsUnparsed. - A custom
ITikTypeConverterthat throws for a word it cannot hold makes that valueUnparsedinstead of failing the load. -
TikValue<T>serializes with System.Text.Json on net8.0. Newtonsoft.Json and netstandard2.0 have no converter: serialize.Value.
A class may mix plain and TikValue<T?> properties. See Custom entities.
Telnet, Ssh, WinboxCli and WinboxNative throw the SocketException itself, as the binary API always did,
instead of wrapping it in TikConnectionLoginException ("Cannot log in"). An SSH connect timeout is a
SocketException with SocketError.TimedOut. A catch (TikConnectionLoginException) meant for "the router is not
there" needs a catch (SocketException) as well. Rest keeps its IOException.
Where RouterOS completes a unique word, or the prefix all candidates share, in place instead of listing,
CompleteCli returns nothing and CompleteCliRaw returns the completed line. 4.x returned the terminal's echo as
tokens.
4.x reported it as an incomplete read to retry, which could never succeed. The exception carries the router's message.
/interface/ovpn-server/server holds several named servers on current RouterOS 7. OvpnServer has an Id, and is
read with LoadAll, not LoadSingle.
QueueType.Default and BgpInstance.Default have a private setter: the router reports them and no set takes
them.
As on IpDhcpServer.LeaseTime. Assign (TikDuration)TimeSpan.FromHours(1) or (TikDuration)"1h", and read the
length of time as lease.LeaseTime.Value?.Value.
InterfaceBridge.Priority (was string) and BridgePort.Priority (was int) are TikValue<TikHexNumber?>. The API
prints them in hex (0x8000) and the CLI before RouterOS 7.24 in decimal (32768); both read to the same number,
and it is written in hex, because RouterOS 7.24 refuses a decimal port priority. Assign new TikHexNumber(0x80),
and read the number as port.Priority.Value?.Value. See Entity value types.
- RoMON: reach a router through a neighbouring MikroTik. See RoMON connection.
-
Ordered lists:
TikListMergeandSaveListDifferencesreorder with the fewest moves and create rows in place; the list writers have async twins. See TikListMerge. -
Renamed fields read on both RouterOS versions (
TikPropertyAttribute.AlternateNames), andTikPropertyAttribute.WinboxLabelnames a field for the WinBox native transport.
Open an issue on GitHub with the compiler error and the line it points at.
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