DotNetWorkQueue.Aq.ExpressionJsonSerializer
1.2.0
dotnet add package DotNetWorkQueue.Aq.ExpressionJsonSerializer --version 1.2.0
NuGet\Install-Package DotNetWorkQueue.Aq.ExpressionJsonSerializer -Version 1.2.0
<PackageReference Include="DotNetWorkQueue.Aq.ExpressionJsonSerializer" Version="1.2.0" />
<PackageVersion Include="DotNetWorkQueue.Aq.ExpressionJsonSerializer" Version="1.2.0" />
<PackageReference Include="DotNetWorkQueue.Aq.ExpressionJsonSerializer" />
paket add DotNetWorkQueue.Aq.ExpressionJsonSerializer --version 1.2.0
#r "nuget: DotNetWorkQueue.Aq.ExpressionJsonSerializer, 1.2.0"
#:package DotNetWorkQueue.Aq.ExpressionJsonSerializer@1.2.0
#addin nuget:?package=DotNetWorkQueue.Aq.ExpressionJsonSerializer&version=1.2.0
#tool nuget:?package=DotNetWorkQueue.Aq.ExpressionJsonSerializer&version=1.2.0
DotNetWorkQueue.Aq.ExpressionJsonSerializer
Expression tree serializer/deserializer for Newtonsoft.Json.
Fork of aquilae/expression-json-serializer, created primarily to make the serializer thread-safe — upstream shares mutable dictionaries across threads, which is unusable under a concurrent consumer. Also adds multi-targeting. Published for use by DotNetWorkQueue.
Install
dotnet add package DotNetWorkQueue.Aq.ExpressionJsonSerializer
Supported targets
- .NET 10.0
- .NET 8.0
- .NET Framework 4.8
- .NET Standard 2.0
Usage
var settings = new JsonSerializerSettings();
settings.Converters.Add(new ExpressionJsonConverter(typeof(MyMessage)));
Expression<Func<MyMessage, bool>> expr = m => m.Value > 10;
string json = JsonConvert.SerializeObject(expr, settings);
var restored = JsonConvert.DeserializeObject<Expression<Func<MyMessage, bool>>>(json, settings);
Changes from upstream
- Thread-safe: all internal dictionaries use
ConcurrentDictionary. This is the main reason the fork exists — upstream's shared mutable caches corrupt under concurrent use. - Added net10.0, net8.0, net48, and netstandard2.0 multi-targeting
- Merged partial loop and goto expression support (see limitation below)
- Added NuGet packaging and GitHub Actions CI/publish pipeline
Known limitations
DebugInfoExpression and DynamicExpression are not supported and will not be — they
throw NotSupportedException with an explanation. These are not gaps waiting to be filled:
DebugInfoExpressioncarries aSymbolDocumentInfo— source file name, language GUID, checksum — which conveys sequence-point information to a debugger attached to the originating assembly. It has no meaning once the tree is rebuilt elsewhere. Strip debug info before serializing.DynamicExpressioncarries aCallSiteBinder, a runtime object encoding language-specific call-site semantics together with its own cache state. There is no general way to write one out and rebuild an equivalent binder in another process. Resolve the dynamic call to a concreteMethodCallExpressionbefore serializing.
Everything else round-trips on every target, including full statement-expression support: blocks, loops with break and continue, goto/label, switch, try with catch/filter/fault/ finally, collection and object initialisers, and indexers.
Payload compatibility
The JSON payload carries no format version, and 1.2.0 changes how two node types are written.
ListInit and MemberInit — collection and object initialisers, new List<int> { 1, 2 }
and new Thing { A = 1 } — used to be stored in the lowered form the compiler reduces them
to: a block with a temporary variable. From 1.2.0 they are written natively. That change is
what makes them round-trip on .NET Framework 4.8, where the lowered form was not portable.
- Reading is backward compatible. 1.2.0 still deserializes payloads written by 1.1.0 and earlier, including the lowered block form.
- Writing is not forward compatible. A payload written by 1.2.0 containing a collection or object initialiser cannot be read by 1.1.0 or earlier.
If payloads outlive the process that wrote them — a queue, a cache, a database column — then upgrade readers before writers. For a DotNetWorkQueue deployment that means consumers first, then producers. Upgrading producers first will fail any queued message containing an initialiser as soon as a not-yet-upgraded consumer picks it up.
Expressions without initialisers are unaffected in both directions.
Build and CI
| Pipeline | What it does |
|---|---|
.github/workflows/ci.yml |
Builds the solution and runs the test suite on net10.0 / net8.0 (ubuntu) and net48 (windows). Also packs and publishes to NuGet on a v* tag. |
.github/workflows/sonarcloud.yml |
SonarCloud CI-based analysis. Builds, runs the net10.0 tests with coverlet (OpenCover format), and feeds coverage to SonarCloud. Requires the SONAR_TOKEN repository secret. Rule exclusions are documented in docs/sonar-exclusions.md. |
Jenkinsfile |
Jenkins pipeline that fires on every PR (reported as the continuous-integration/jenkins/pr-merge check). Builds and runs the net10.0 / net8.0 test stages on a Docker agent. |
To reproduce the Sonar coverage run locally:
dotnet build Aq.ExpressionJsonSerializer.sln -c Debug
dotnet test Aq.ExpressionJsonSerializer.Tests/Aq.ExpressionJsonSerializer.Tests.csproj \
-f net10.0 -c Debug --no-build \
--collect:"XPlat Code Coverage;Format=opencover"
Publishing a release
Publishing uses NuGet Trusted Publishing over GitHub OIDC — there is no long-lived API key. The publish job exchanges a short-lived OIDC token for a NuGet key valid for one hour, immediately before pushing.
- Push a version tag:
git tag v1.0.0 && git push origin v1.0.0 - GitHub Actions runs build and tests across all targets, then packs and pushes to nuget.org automatically.
Trusted publishing setup
A one-time policy on nuget.org (your username → Trusted Publishing) backs this:
| Field | Value |
|---|---|
| Policy owner | blehnen |
| Repository Owner | blehnen |
| Repository | expression-json-serializer |
| Workflow File | ci.yml (filename only, no path) |
| Environment | (blank — the publish job declares no GitHub environment) |
To confirm the policy resolves without cutting a release, run the CI workflow manually
from the Actions tab. That triggers the verify-trusted-publishing job, which performs the
OIDC exchange and stops — it never pushes a package. Do this after any change to the policy
or to the workflow filename, since the policy is bound to ci.yml by name.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 is compatible. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.8
- Newtonsoft.Json (>= 13.0.4)
-
.NETStandard 2.0
- Newtonsoft.Json (>= 13.0.4)
-
net10.0
- Newtonsoft.Json (>= 13.0.4)
-
net8.0
- Newtonsoft.Json (>= 13.0.4)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DotNetWorkQueue.Aq.ExpressionJsonSerializer:
| Package | Downloads |
|---|---|
|
DotNetWorkQueue
Work queue for dot net 8.0 and 10.0. Supports scheduling, delayed processing, prioritized queues, message expiration, retries with configurable back-off and more. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on DotNetWorkQueue.Aq.ExpressionJsonSerializer:
| Repository | Stars |
|---|---|
|
blehnen/DotNetWorkQueue
A work queue for dot.net with SQL server, SQLite, LiteDB, Redis and PostGreSQL backends
|
1.2.0
PAYLOAD COMPATIBILITY: this release changes how collection and object initialisers (ListInit, MemberInit) are written. Reading is backward compatible - 1.2.0 still deserializes payloads written by 1.1.0 and earlier. Writing is NOT forward compatible - a payload written by 1.2.0 containing an initialiser cannot be read by 1.1.0 or earlier. If payloads outlive the process that wrote them (a queue, a cache, a database column), upgrade readers before writers: for DotNetWorkQueue that means consumers before producers. Expressions without initialisers are unaffected.
- Fixed: ListInit and MemberInit expressions now round-trip on .NET Framework 4.8. They were previously stored in the lowered form the compiler reduces them to, and on .NET Framework that lowering produces a block declaring no variables while its body still references the temporary, so the deserialized tree failed to compile. They are now serialized natively.
- Changed: DebugInfoExpression and DynamicExpression now throw NotSupportedException with an explanation of why they cannot be supported and what to do instead, rather than a bare NotImplementedException. Note this is a different exception type; a catch of NotImplementedException specifically will no longer match.
- Fixed: DynamicExpression is refused before reduction, so callers get that explanation instead of a Newtonsoft "Self referencing loop detected" error from deep inside the binder object graph.
- Fixed: three handlers tested the wrong expression type and so never matched the node they are named for (DebugInfo, ListInit, and one corrected earlier).
- Performance: per-call parameter and label maps use Dictionary rather than ConcurrentDictionary. Those instances are never shared across threads, so the lock array ConcurrentDictionary allocates per call bought nothing. Roughly 2-4% less time and allocation. The shared reflection caches remain concurrent.
- Test coverage raised to 89%.
1.1.0
- Implemented LabelExpression, SwitchExpression and TryExpression. Goto/label blocks now round-trip; previously only Expression.Loop with break/continue worked.
- Fixed serialization of unnamed parameters. Reduced nodes (compound assignment, increment/decrement) produce temporaries with no name, which were written as null and could not be read back.
- Fixed a race in label target creation: ContainsKey-then-index on a ConcurrentDictionary could return two different LabelTargets for one name.
- Throw TypeLoadException / MissingMethodException instead of bare System.Exception for unresolvable types and constructors.
- Test coverage raised from 62% to 87%.