Skip to content

Modernize dependency injection and JSON serialization - #368

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

Modernize dependency injection and JSON serialization#368
niemyjski wants to merge 13 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

Strict review hardening

  • detect constructor and factory DI cycles deterministically without hanging
  • scope cycle tracking to each logical resolution flow so concurrent use of a captured Microsoft service provider cannot produce false circular-dependency failures
  • keep failed extended-data serialization diagnostics as strings instead of incorrectly marking them as raw JSON, including MessagePack round trips
  • retain the public getter-only ClientConfiguration.Settings contract while allowing source-generated STJ deserialization
  • apply the legacy ignore attribute at the filtered writer boundary without mutating caller-owned JsonTypeInfo
  • prevent null or unstructured POST data from recursively invoking its converter
  • split the oversized serializer test class into a partial compatibility-focused file
  • declare the existing root Markdown documentation as the modern NuGet package readme while retaining the legacy readme.txt

Why

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

Impact

  • ApiCompat is clean for prior netstandard2.0 -> current netstandard2.0/net8.0 and prior net462 -> current net462.
  • 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 about 17% larger overall because it contains more TFMs and the package readme, while the individual netstandard2.0 assembly is about 65% smaller after removing the vendored Newtonsoft source.
  • Modern target assets use normal runtime stack traces instead of the legacy IL/PDB demystifier.
  • 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: 377 core tests + 13 MessagePack tests passed; 0 failed; 18 existing skips
  • Cobertura: 74.2% line / 50.1% branch overall; 88.9% of changed executable production lines covered
  • critical coverage: filtered writer 97.2% line / 92.5% branch, DataDictionary 98.9% / 92.0%, JSON element conversion 95.7% / 95.7%, DI resolver 85.8% / 64.5%, and DI fallback 89.5% / 80.0%
  • Windows-shaped full solution build: net462, net472, netstandard2.0, net8.0, net8.0-windows, net9.0, and net10.0; 0 warnings, 0 errors
  • uniquely versioned packed-package NativeAOT executables: net8.0 and net10.0 passed, including default DI, built-in configuration deserialization, source-generated custom payload converters/fields, compatibility-ignore filtering, storage, queues, submission, batches, and exception frames
  • Generic Host NativeAOT executables: net8.0 and net10.0 passed
  • current direct and transitive NuGet vulnerability audit: no vulnerable packages
  • final NuGet pack: passed without warnings; both README.md and legacy readme.txt are present
  • git diff --check: passed; no changed C# file exceeds 1,000 lines

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 package and host executables pass on net8.0 and net10.0 with ILLink/trim-analysis warnings treated as errors

CI status

  • Linux, macOS, and Windows passed in both push and pull-request workflows on commit d94a17b
  • 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 12 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