-
Notifications
You must be signed in to change notification settings - Fork 98
Upgrading from 3.x to 4.0 with an AI agent
If your project is worked on with an AI coding agent — Claude Code, GitHub Copilot, Cursor, Aider, Windsurf, and the like — you can hand it the whole 3.x → 4.0 upgrade. The prompt below is the Upgrading from 3.x to 4.0 guide rewritten as a step-by-step task list an agent can execute: fix the package references (and everything else that named the old package layout by hand), rename the three callback methods, opt back into the two flipped run-time defaults, and work through the now-nullable entity reads, then build and verify.
It was first written from a real upgrade — a small SDK-style console app that streams /tool/torch over
API-SSL — where the diff was three connection-setup edits, one LoadAsync → LoadWithCallback rename, a
bool? guard and a few string? guards. It has since been re-run on a .NET Framework 4.7.2
Windows-service solution that referenced tik4net as a source-tree ProjectReference and shipped
through a hand-written WiX installer, and the prompt was extended to cover what that surfaced: non-SDK
project mechanics, .sln cleanup, and deployment manifests that list assemblies by hand. Nothing is
renamed at the namespace or type level, so there is no find-and-replace for the agent to get wrong.
- Commit or stash first, so the agent's changes land on a clean tree you can review.
- Point the agent at this repository's
.csproj/.vbprojfiles, its.sln, and any installer / packaging project, and let it read the linked wiki pages — the prompt names them.
-
Read the diff. The mechanical parts (package refs, the rename) are safe; the judgement calls are
where a now-nullable field should fall back to a default versus be treated as always-present, and
where direct-
newentity saves need an explicit field filter. -
Build every configuration. SDK-style projects:
dotnet buildfor each. Non-SDK projects (.NET Framework, WiX, old WinForms/WCF):dotnetwill not build them — usemsbuildfrom Visual Studio / Build Tools. A repo that switches between a localProjectReferenceand a NuGetPackageReferenceby an MSBuild condition has two configs; build both. -
Rebuild the installer / package if the project ships one with a hand-maintained file list —
tik4net.objects.dllis a new file it must now include, and a miss only shows up as aFileNotFoundExceptionat run time on the deployed box. - Run your tests, or note that they only compile if they need live hardware.
-
Smoke-test a real connection if a RouterOS device is reachable. The TLS default change (invalid
certificates are now rejected) is a run-time failure that neither the compiler nor the agent can see;
it only shows up when an
ApiSsl/RestSslconnection actually opens against a self-signed cert.
Copy everything in the box below.
# Agent task: upgrade a .NET project from tik4net 3.x to tik4net 4.0
You are upgrading a .NET codebase from **tik4net 3.6.x** to **tik4net 4.0** (published as
the `4.0.0-alphaN` prerelease line — resolve the current version yourself, see Step 1).
Work through the steps below in order. Do not skip the verification build at the end.
Authoritative references — read them before touching code:
- Upgrade guide: <https://github.com/danikf/tik4net/wiki/Upgrading-from-3.x-to-4.0>
- Release notes: <https://github.com/danikf/tik4net/releases>
- Version history: <https://github.com/danikf/tik4net/wiki/History>
Namespaces (`tik4net`, `tik4net.Objects`, …), type names and assembly file names are
**unchanged**. Nothing is a find-and-replace of a namespace. What changes is package
layout, two runtime defaults, three renamed methods, a large set of entity properties that
became nullable — and anywhere your build or your packaging named the old package or the
old single assembly by hand.
---
## Step 0 — Identify the project shape and pick the toolchain
Before editing anything, establish two things.
**SDK-style or not.** Open each `.csproj`. `<Project Sdk="Microsoft.NET.Sdk">` on the
first line → SDK-style: the `dotnet` CLI works for restore, build and package management.
No `Sdk` attribute, an explicit `<Compile Include>` line for every source file,
`packages.config`, `<TargetFrameworkVersion>v4.x</TargetFrameworkVersion>` → non-SDK
(typically .NET Framework, WinForms / WPF / WCF, WiX). For non-SDK projects:
- **`dotnet build` / `dotnet restore` / `dotnet add package` do not work.** Use MSBuild
from Visual Studio or Build Tools: `msbuild YourSolution.sln -t:Restore`, then
`msbuild YourSolution.sln -p:Configuration=Debug`. (`nuget restore` also works.)
- Package references are changed by **editing the `.csproj` XML by hand** (or
`packages.config`), not with `dotnet` / `nuget` commands.
- A source file you add is **not compiled** until you add a `<Compile Include="…" />` line
for it.
**Where tik4net is referenced.** Grep the whole repository, not just the obvious project:
tik4net — package refs, project refs, using, .sln entries
tik4net\.objects|tik4net\.entities — the old mapper package / assembly
TikConnectionType\.|ConnectionFactory — connection sites (which transports; SSL?)
LoadAsync|LoadListenAsync|ExecuteAsync\( — the renamed callbacks (Step 3)
\.Disabled\b|\.Save\(|ExecuteScalar\( — nullable reads / Save / ExecuteScalar (Steps 6-7)
Then grep the literal string `tik4net` in **`.wxs` / `.wixproj` / `.nuspec` / `Dockerfile`
/ ClickOnce `.application` / `.pubxml` / publish or xcopy scripts** — anywhere a list of
assemblies to ship is maintained by hand. Step 1c explains why.
## Step 1 — Fix package references, and everything else that named the old layout
### 1a. The reference itself
In 3.x the O/R mapper was a separate package. Since 4.0 `tik4net.objects.dll` ships
**inside** the `tik4net` NuGet package and there is no separate package any more. A
leftover reference to the mapper gives you the same assembly from two sources — the build
fails with a conflict, or worse, silently binds a 3.x mapper against the 4.0 core.
Remove **every** form of the mapper reference:
- `PackageReference` / `packages.config` entry for `tik4net.objects` (the 3.x mapper package).
- `PackageReference` for `tik4net.entities` (the name the mapper package had during the very
first 4.0 alpha; later alphas fold it into `tik4net`).
- `ProjectReference` to `…/tik4net.objects/tik4net.objects.csproj` **and** to
`…/tik4net/tik4net.csproj` when the dependency was a checkout of the tik4net **source
tree** rather than a package. Both go; the one package replaces them.
Keep / add a single `tik4net` reference at the target version:
```diff
- <PackageReference Include="tik4net" Version="3.6.0" />
- <PackageReference Include="tik4net.entities" Version="3.6.0" />
+ <PackageReference Include="tik4net" Version="4.0.0-alphaN" />
```
**Pin an explicit version.** It is a prerelease, so a floating `*` will not pick it up
without `--prerelease` / source-mapping tweaks. Resolve the current version from
<https://api.nuget.org/v3-flatcontainer/tik4net/index.json> (or the NuGet UI) and use that
exact string — do not copy a version number out of this document.
On SDK-style projects `dotnet remove package tik4net.objects && dotnet remove package
tik4net.entities && dotnet add package tik4net --prerelease --version <resolved>` does the
edit. On non-SDK projects, edit the XML by hand.
More gotchas:
- If the project keeps **both** a local `ProjectReference` and a `PackageReference`, split
by an MSBuild condition (e.g. `'$(CI)' == 'true'`), update **both** arms — the source
checkout must be on a 4.0 branch/tag, the package arm gets the resolved NuGet version.
GitHub Actions sets `CI=true`, so that condition is already live on every CI build.
- The `tik4net.objects` package ID on nuget.org is squatted by a third party — never re-add it.
### 1b. The solution file
If you removed `ProjectReference`s to a tik4net source checkout, the `.sln` still lists
those projects. Remove, for each: the `Project("…") = "tik4net", …` /
`"tik4net.objects", …` block, its lines in `GlobalSection(ProjectConfigurationPlatforms)`,
and any `GlobalSection(NestedProjects)` entry that filed it under a solution folder (drop
the now-empty folder too). A stale `.sln` entry pointing at a csproj you no longer build is
a load error in Visual Studio and an MSBuild warning on the command line.
### 1c. Hand-maintained deployment manifests ← easy to miss; fails only at run time
Before 4.0 the mapper lived inside `tik4net.dll`; now `tik4net.objects.dll` is a **second
file next to it**. NuGet / `dotnet` copy both to `bin/` automatically, but anything that
enumerates assemblies **by hand** still lists only `tik4net.dll` and will ship a build that
throws `FileNotFoundException: tik4net.objects` on first use:
- WiX (`.wxs` `<File>` elements), Inno Setup, InstallShield file lists
- ClickOnce `.application` / `.manifest`
- `Dockerfile` `COPY` lines that name individual DLLs
- `.nuspec` `<files>` lists, if you re-package tik4net into your own package
- publish / xcopy / robocopy scripts
Add `tik4net.objects.dll` (and its `.xml` doc, if you ship docs) everywhere `tik4net.dll`
appears. While there, confirm the other tik4net runtime deps travel too — `System.Text.Json`
on the `netstandard2.0` leg.
### 1d. A wrapper library in between
If a **shared library of yours** references tik4net and your app references that library
(not tik4net directly), upgrade the shared library **in the same change**: bump its
`tik4net` reference, rebuild it, make sure its consumers pick up the new binary. A 3.x
wrapper against a 4.0 core is the split-assembly hazard from 1a, one level down.
### 1e. Restore from a clean slate
After changing references: `dotnet restore --force` (SDK), or delete `obj/` +
`project.assets.json` and run `msbuild -t:Restore` (non-SDK). NuGet caches the old
resolution and can otherwise keep a stale `tik4net.objects.dll` in `bin/`.
### 1f. If you target .NET 8+
The 4.0 package carries a `net8.0` lib with **zero** runtime dependencies
(`System.Text.Json` is referenced only on the `netstandard2.0` leg). Nothing to do; expect
the dependency graph to shrink.
## Step 2 — `using` directives
Nothing to change. `using tik4net;`, `using tik4net.Objects;`,
`using tik4net.Objects.Ip;` etc. all still resolve, and the mapper assembly is still
`tik4net.objects.dll` (it is just delivered inside the `tik4net` package now).
## Step 3 — Rename the three callback methods (hard compile errors)
The old names are `[Obsolete(..., error: true)]` — they will not compile.
| 3.x | 4.0 |
|---|---|
| `connection.LoadAsync<T>(callback, …)` / `command.LoadAsync<T>(callback, …)` | `LoadWithCallback<T>` |
| `connection.LoadListenAsync<T>(…)` / `command.LoadListenAsync<T>(…)` | `LoadListenWithCallback<T>` |
| `command.ExecuteAsync(callback)` | `ExecuteWithCallback` |
The signatures are otherwise identical — it is a pure rename at the call site. Do **not**
"fix" them by switching to the new awaitable `LoadListAsync` / `LoadAllAsync` /
`ExecuteScalarAsync` unless you actually want to restructure that code around `await`;
those return `Task` and do not take a callback. The callback forms still return a running
`ITikCommand` you keep a handle to and later `CancelAndJoin(...)`.
Also: the old 3.x `CallCommandAsync` that returned a `System.Threading.Thread` is gone.
The name now denotes an awaitable low-level call on `ITikRawSentenceConnection` returning
`Task<IList<ITikSentence>>`. If code used the `Thread`-returning one, move it to
`ITikCommand.ExecuteWithCallback`.
## Step 4 — Runtime default: invalid TLS certificates are now rejected
`AllowInvalidCertificate` now defaults to **`false`** (was effectively `true` for
API-SSL), and in 4.0 it applies to **API-SSL as well as REST-SSL** (through 3.x it only
reached REST). RouterOS devices present a **self-signed** certificate by default, so a
program that opened an `ApiSsl` connection in 3.x without configuring anything will now
fail to connect with a TLS error.
The short `ConnectionFactory.OpenConnection(type, host, port, user, pass)` overloads have
nowhere to pass this. Switch those call sites to build a `TikConnectionSetup`:
```csharp
var setup = new TikConnectionSetup(host, user, pass)
{
Port = port, // omit to use the transport default (API 8728/8729)
AllowInvalidCertificate = true, // RouterOS self-signed cert; or pin it, see below
};
using var conn = setup.Create(connType); // connType: TikConnectionType.ApiSsl / .Api / …
```
Better than blanket-trusting, if the router identity is known — pin the certificate and
leave `AllowInvalidCertificate` alone (the callback wins outright when set):
```csharp
CertificateValidationCallback = (sender, cert, chain, errors) =>
cert != null && cert.GetCertHashString() == expectedThumbprint,
```
`ConnectionFactory` still exists and is still supported (it builds a `TikConnectionSetup`
internally); there are new `OpenConnection(type, setup)` / `CreateConnection(type, setup)`
overloads so you don't have to abandon it. Plain (non-SSL) `Api` connections are
unaffected — but setting `AllowInvalidCertificate = true` on them is harmless.
**Find every SSL connection site**, not just the obvious one — e.g. a separate
"list interfaces" / "test credentials" helper that opens its own connection. If the grep
from Step 0 shows only `TikConnectionType.Api` (no `ApiSsl` / `RestSsl` / `Rest`), this
step is a no-op for you — note that and move on.
## Step 5 — Runtime default: synchronous commands are tagged
`SendTagWithSyncCommand` now defaults to **`true`** on the binary API (every command,
including login, carries a `.tag`). This is what makes one connection safe to use from
several threads. **Normally nothing to do.** Two cases where it matters:
- You have a **fake/scripted RouterOS** in tests: it must **echo the request's tag back**
in replies, or the caller waits out its receive timeout.
- You need the exact 3.x bytes on the wire: set
`new TikConnectionSetup(...) { SendTagWithSyncCommand = false }`. The property moved off
`ITikConnection` onto `ITikTaggedConnection` (pattern-match or set it via the setup).
## Step 6 — Nullable entity properties (compile errors when *reading*)
4.0 makes the mapper honest about what RouterOS actually sends. Assignments are
unaffected; **reading** a value at the old type is a compile error.
### 6a. Every *writable* `bool` on an entity is now `bool?`
Three router states (`yes` / `no` / unset) don't fit in `bool`. Read-only flags
(`Running`, `Dynamic`, `Invalid`, …) stay `bool`.
```csharp
// was: if (addr.Disabled) // no longer compiles
if (addr.Disabled == true) … // "router says disabled"
if (addr.Disabled ?? false) … // when you want a plain bool
bool d = addr.Disabled.GetValueOrDefault();
// combining with a read-only bool:
.Where(a => !(a.Disabled ?? false) && !a.Invalid)
```
Behavioural consequence to keep in mind: an explicitly assigned `false` now actually
reaches the router (in 3.x it was indistinguishable from "untouched" and dropped), and
setting a previously-loaded flag to `null` **unsets** it on the next `Save`.
### 6b. ~26 numeric and ~13 enum properties that carry a router default are nullable
`IpProxy.Port`, `IpSocks.Port`, `ToolEmail.Port`, `InterfaceWireguard.ListenPort`,
`Certificate.DaysValid`, every `Mtu`, `IpsecPolicy.Action`, certificate digest/key-size
enums, etc.
```csharp
int port = proxy.Port ?? 8080; // was: int port = proxy.Port;
int port = proxy.Port.GetValueOrDefault();
if (cert.DigestAlgorithm == Certificate.DigestAlgorithmType.Sha256) … // still fine
```
### 6c. Every mapped *reference-typed* entity property is now nullable (`string?`, …)
A RouterOS record only carries the fields the router sent; a `.proplist` load carries
fewer. So `Interface.Name`, `IpAddress.Address`, `ToolTorch.SrcAddress`/`DstAddress`/
`SrcPort`/`DstPort`, `.Id`, etc. are `string?`.
- If **your** project builds with `<Nullable>enable</Nullable>`, you'll get `CS8600` /
`CS8602` / `CS8604` wherever entity fields flow into non-nullable locals, parameters or
string operations. Fix at the boundary: guard, `?.`, `??`, or `!` where a field is
guaranteed present right after the load. Prefer coalescing to a sensible default
(`?? ""`) when the value feeds keys/formatting.
- If your project does **not** enable nullable, these are invisible — but still review
hot paths where a now-nullable field is dereferenced.
- `ExecuteScalarOrDefault()` / `ExecuteSingleRowOrDefault()` (and their `…Async` forms)
are now annotated to return nullable — they always could return null.
## Step 7 — Other source-level breaks (only if the code touches them)
- **`Interface.Type`, `Interface.MacAddress`, `Interface.FastPath` are read-only.**
`/interface set` never accepted them. Assign on the concrete menu instead
(`InterfaceEthernet.MacAddress` is writable). `Interface` gained `L2Mtu`, which
`/interface set` does accept.
- **`Save()` no longer does a `LoadById` round-trip before `/set`** — it diffs against the
snapshot the loaded entity already holds. Code that relied on `Save()` refreshing the
entity first will behave differently. Restore the old behaviour globally with
`TikDefaults.SaveMode = TikSaveMode.FullUpdate` or per call
(`saveMode: TikSaveMode.FullUpdate`).
**Saving an entity that was never loaded — a `new T { … }` you construct yourself,
whether for `/add` or with a hand-set `.id` for `/set` — has no snapshot to diff
against, so it puts every writable field on the wire whose value differs from that
property's declared default. For non-nullable `long` / `bool` properties without a
`DefaultValue` that means `0` / `no` is sent explicitly.** When you write new
create/update code, pass an explicit field filter —
`connection.Save(entity, new[] { "name", "target", … })` — or go through
`connection.CreateMerge(...)` with `.Field(...)` / `.JustForInsertField(...)` selectors,
which is what the bulk-sync paths already do.
- **`ExecuteScalar()` on a command that returns nothing** (`set`/`unset`/`remove`/
`enable`/`disable`) now throws `TikCommandEmptyResponseException`, not
`TikNoSuchItemException`, and the two no longer share a base type — a
`catch (TikNoSuchItemException)` will not catch it. Use `ExecuteNonQuery()` for those
verbs (correct on 3.x too), or `ExecuteScalarOrDefault()` if you must read a value.
A `print`/`get` that matches nothing still throws `TikNoSuchItemException`.
- **Implementing `ITikConnection` yourself** (custom transport or hand-written test
double): `CallCommandSync`, `SafeMode*`, `SendTagWithSyncCommand` moved to
capability interfaces (`ITikRawSentenceConnection`, `ITikSafeModeConnection`,
`ITikTaggedConnection`); add a `ConnectTimeout` property. To *call* those members off a
plain `ITikConnection`, pattern-match for the facet (`if (conn is ITikRawSentenceConnection raw) …`)
or use the transport's composite interface from its own factory
(`setup.CreateApiConnection()` → `ITikApiConnection`).
- **`tik4net.testing`**: `TikFakeConnection` no longer has `SendTagWithSyncCommand`; its
default capability set now includes `SafeMode`.
## Step 8 — Build, fix, verify
1. Restore from clean: SDK → `dotnet restore --force`; non-SDK → delete `obj/` +
`project.assets.json`, then `msbuild YourSolution.sln -t:Restore`.
2. Build and resolve every error: SDK → `dotnet build`; non-SDK →
`msbuild YourSolution.sln -p:Configuration=Debug`. Errors cluster in: connection setup
(Step 4), the callback rename (Step 3), nullable reads (Step 6), and missing
`<Compile Include>` lines for any file you added (non-SDK).
3. Build **every configuration the repo actually produces**: `Debug` and `Release`; both
arms if a `ProjectReference` / `PackageReference` split exists (e.g.
`CI=true dotnet build -c Release`); and any `publish` step CI runs (single-file /
self-contained flags change the trim/reflection surface).
4. Tests: run the suite if it runs offline. If the project's "tests" need a live router or
database (many internal projects' tests are integration tests), **compiling them as
part of the solution build is the bar** — report that plainly rather than claiming a
green run you did not get.
5. If you touched a deployment manifest (Step 1c), rebuild the installer / package and
confirm `tik4net.objects.dll` is inside it.
6. Smoke-test an actual connection against a RouterOS device if one is reachable — the TLS
default change (Step 4) is a runtime failure the compiler cannot catch. A "clear
console / no TTY" exception under a pipe is not a tik4net failure.
## Step 9 — Update project docs
Update any `CLAUDE.md` / `AGENTS.md` / README that pins the tik4net version or describes
the package layout. Record the non-obvious decisions for the next session: the resolved
version, that the mapper is now bundled (no separate `tik4net.objects` / `tik4net.entities`),
the `AllowInvalidCertificate = true` requirement for API-SSL, the `LoadWithCallback`
rename, and — where they applied — the `.sln` cleanup and the deployment-manifest edit.
---
## Checklist
- [ ] project shape identified; correct toolchain used (`dotnet` vs MSBuild)
- [ ] mapper reference removed in **every** form — package, `packages.config`, and
source-tree `ProjectReference` to `tik4net.csproj` + `tik4net.objects.csproj` — in
all build arms
- [ ] single `tik4net` 4.0 reference, explicit resolved prerelease version
- [ ] `.sln` entries for removed tik4net source projects deleted (+ empty solution folder)
- [ ] hand-maintained deployment manifests (WiX / ClickOnce / Docker / nuspec / scripts)
carry `tik4net.objects.dll`
- [ ] wrapper library in between (if any) upgraded in the same change
- [ ] restore from a clean `obj/` done
- [ ] `LoadAsync` → `LoadWithCallback`, `LoadListenAsync` → `LoadListenWithCallback`,
`ExecuteAsync(cb)` → `ExecuteWithCallback`
- [ ] every API-SSL / REST-SSL connection site sets `AllowInvalidCertificate = true`
(or a pinning callback) via `TikConnectionSetup`
- [ ] nullable `bool?` / numeric? / `string?` entity reads fixed
- [ ] `Save()` / `ExecuteScalar()` semantics reviewed; direct-`new` create/update code
uses an explicit field filter or `CreateMerge`
- [ ] custom `ITikConnection` impls / fakes updated (tag echo!)
- [ ] Debug + Release (+ publish, + both ref arms) build; tests run, or compile-only noted
- [ ] runtime smoke test against a router (TLS default)
- [ ] project docs / memory updated
See Upgrading from 3.x to 4.0 for the same material as a narrative with the reasoning behind each change, and History for the full 4.0 changelog.