Skip to content

Modernize dependency injection and JSON serialization#368

Draft
niemyjski wants to merge 10 commits into
mainfrom
feature/modern-di-stj
Draft

Modernize dependency injection and JSON serialization#368
niemyjski wants to merge 10 commits into
mainfrom
feature/modern-di-stj

Conversation

@niemyjski

@niemyjski niemyjski commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

  • replace the vendored Newtonsoft.Json fork with System.Text.Json
  • replace TinyIoC with Microsoft.Extensions.DependencyInjection while preserving the existing resolver API
  • add source-generated serialization metadata, trimming annotations, and package/host NativeAOT smoke tests
  • preserve legacy payload behavior for exclusions, depth, cycles, raw DataDictionary values, named floating-point values, public fields, and the Exceptionless ignore attribute
  • honor STJ property contracts in filtered serialization, including ignore predicates, custom converters, number handling, extension data, and declared-type depth behavior
  • isolate Generic Host and logging clients so provider-owned clients cannot mutate or dispose the process-wide default client
  • collect and upload Cobertura coverage in Linux CI

Why

The original Newtonsoft.Json + TinyIoC client did not work under NativeAOT. Changing only the serializer or only the DI container was also insufficient: the serializer, DI activation, custom payload metadata, configuration activation, and runtime stack-frame handling all needed coordinated hardening.

Impact

  • ApiCompat is clean for the previous netstandard2.0 API against the new netstandard2.0 and net8.0 assets.
  • Custom payloads follow System.Text.Json contracts. Consumers relying on Newtonsoft-specific DataContract/DataMember behavior should migrate to STJ attributes and register source-generated metadata for NativeAOT payloads.
  • Microsoft.Extensions.DependencyInjection becomes transitive. System.Text.Json 10 is used on net462, netstandard2.0, and net8.0.
  • The DI container remains isolated per Exceptionless client; an application's root provider is not imported automatically.
  • Configuration-based hosting and logging overloads now own isolated clients. Explicit client overloads remain caller-owned. Code that intentionally relied on those overloads mutating ExceptionlessClient.Default will observe the corrected isolated behavior.
  • The package adds modern target assets and NativeAOT support. It is larger overall because it contains more TFMs, while the individual netstandard2.0 assembly is substantially smaller after removing the vendored Newtonsoft source.
  • This should ship as 7.0 because serializer semantics, dependencies, target assets, ownership behavior, and modern stack-trace behavior change even though the public API check is clean.

Validation

  • definitive non-Windows suite: 370 core tests + 12 MessagePack tests passed; 0 failed; 18 existing skips
  • Cobertura: 74% line / 50% branch overall; 88.1% of changed executable production lines covered
  • critical coverage: filtered writer 97% line / 93% branch, DataDictionary 99% / 92%, JSON element conversion 96% / 96%, logging provider 100% / 100%
  • Windows-shaped full solution build: net462, net472, netstandard2.0, net8.0, net8.0-windows, net9.0, and net10.0; 0 warnings, 0 errors
  • packed-package NativeAOT executable: net8.0 and net10.0 passed, including source-generated custom payload converters, storage, queues, submission, batches, and exception frames
  • Generic Host NativeAOT executable: net8.0 and net10.0 passed
  • ApiCompat: clean for prior netstandard2.0 -> current netstandard2.0/net8.0, and prior net462 -> current net462
  • Windows-shaped package contains net462, netstandard2.0, net8.0, and net10.0
  • current transitive NuGet vulnerability audit: no vulnerable packages
  • workflow YAML parsing and git diff --check: passed

Historical NativeAOT result

State Result
Newtonsoft.Json + TinyIoC published with 10 warnings, then failed at runtime
System.Text.Json + TinyIoC still failed
System.Text.Json + Microsoft DI before AOT hardening 4 DI warnings, then the serializer constructor was trimmed
Hardened System.Text.Json + Microsoft DI warning-as-error package and host executables pass on net8.0 and net10.0

CI status

  • Linux, macOS, and Windows passed for both push and pull-request workflows
  • real Windows net462/net472 runtime tests passed
  • the Windows prerelease package build and publishing path passed
  • CLA passed; no review comments or unresolved threads

Follow-up

  • consider requiring the platform build checks in branch protection; currently only license/cla is required
  • the PR remains draft for maintainer review of the 7.0 release and ownership changes

niemyjski and others added 10 commits July 12, 2026 13:31
- Remove vendored Newtonsoft.Json (entire src/Exceptionless/Newtonsoft.Json/ directory)
- Remove update-json.ps1 script
- Add System.Text.Json 10.0.0 NuGet package reference
- Rewrite DefaultJsonSerializer using System.Text.Json with:
  - SnakeCaseNamingPolicy matching legacy Newtonsoft behavior (e.g. OSName -> o_s_name)
  - Per-type snake_case applied only to Exceptionless.Models namespace
  - DataDictionaryConverter for storing complex values as JSON strings
  - SettingsDictionaryConverter for ObservableDictionary-based type
  - ObjectToInferredTypesConverter for proper type inference
  - PostDataConverter to convert object/array PostData to indented strings
  - Custom WriteValue with depth limiting and property exclusion support
- Update all model classes to remove [JsonObject] attributes
- Add [JsonPropertyName] for EnvironmentInfo OS properties
- Update DefaultSubmissionClient to use JsonDocument instead of JObject
- All 300 tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sion bug

- Replace custom SnakeCaseNamingPolicy with built-in JsonNamingPolicy.SnakeCaseLower
  (aligns with server approach in exceptionless/Exceptionless#2135)
- Delete unused SnakeCaseNamingPolicy.cs
- Fix DataDictionaryConverter.Write: use WriteRawValue for JSON strings that
  were previously objects (fixes double-escaping on storage roundtrip)
- Fix exclusion logic: add TypeInfoResolver = new DefaultJsonTypeInfoResolver()
  so GetTypeInfo() works and WriteValue can filter properties by name
- Update all test assertions to expect snake_case property names

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WriteValue for IDictionary entries wrote the property name before checking
whether the value could actually be serialized at the current depth. When a
complex value exceeded maxDepth, WriteValue returned without writing anything,
leaving the JSON writer in an invalid state. The error was silently swallowed
by continueOnSerializationError, causing a fallback to full serialization
(effectively ignoring the depth limit entirely).

Fix: check depth before writing the property name. Skip complex dictionary
entries that would exceed maxDepth, consistent with the object property path.

Added regression test that fails before the fix (depth limit violated) and
passes after.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- preserve literal JSON-looking strings in DataDictionary as strings
- restore raw JSON emission only for values produced from structured data
- preserve raw JSON markers through MessagePack storage roundtrips
- coerce primitive SettingsDictionary JSON values to strings like main
- keep dictionary depth-limit regression coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant