Skip to content

Replace JSON.NET with System.Text.Json across the codebase#2135

Open
niemyjski wants to merge 83 commits into
mainfrom
feature/system-text-json-v2
Open

Replace JSON.NET with System.Text.Json across the codebase#2135
niemyjski wants to merge 83 commits into
mainfrom
feature/system-text-json-v2

Conversation

@niemyjski
Copy link
Copy Markdown
Member

@niemyjski niemyjski commented Feb 28, 2026

Replace JSON.NET with System.Text.Json

Replaces all Newtonsoft.Json serialization with System.Text.Json (STJ) and migrates from NEST to Elastic.Clients.Elasticsearch.

Key changes

Serialization

  • All JSON serialization now uses STJ with SnakeCaseLower naming policy
  • ObjectToInferredTypesConverter handles object-typed properties (replaces DataObjectConverter)
  • [JsonExtensionData] on Event merges root-level @error, @request, etc. into Data dict
  • [JsonPropertyName] overrides for legacy property names (o_s_name, o_s_version)
  • V1 webhook models use [JsonPropertyName] for PascalCase backward compatibility

Elasticsearch

  • Migrated from NEST to Elastic.Clients.Elasticsearch throughout
  • All index configurations use new fluent API
  • Migrations and jobs updated for new ES client
  • Repository queries migrated to Foundatio expression-based API

Event processing

  • All event upgraders migrated from JObject to JsonObject (System.Text.Json.Nodes)
  • All plugins migrated from Newtonsoft serializer to STJ
  • GetValue<T>() simplified — removed unnecessary TryDeserializeWithFallback (PascalCase data never existed in ES)
  • Error write-back fix in ErrorPlugin / SimpleErrorPlugin after pipeline mutation

Removed

  • DataObjectConverter, ElasticJsonNetSerializer, all Newtonsoft classes
  • Foundatio.JsonNet, NEST.JsonNetSerializer, FluentRest.NewtonsoftJson packages
  • Unnecessary PascalCase fallback machinery

Test results

  • 1549 total, 0 failed, 1547 passed, 2 skipped
  • New: 27 serializer tests, model serialization tests, query migration tests, integration tests

Comment thread src/Exceptionless.Core/Extensions/JsonNodeExtensions.cs Fixed
Comment thread tests/Exceptionless.Tests/Serializer/SerializerTests.cs Fixed
Comment thread tests/Exceptionless.Tests/Serializer/SerializerTests.cs Fixed
Comment thread src/Exceptionless.Core/Jobs/EventPostsJob.cs Fixed
Comment thread src/Exceptionless.Web/Controllers/EventController.cs Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates application-layer JSON usage from Newtonsoft.Json to System.Text.Json (STJ), aligning core models/plugins/tests and adding STJ-based infrastructure for Elasticsearch/NEST while keeping Newtonsoft only as a transitive dependency for the NEST wire protocol.

Changes:

  • Introduces STJ-based IElasticsearchSerializer implementation and STJ type metadata modifiers to match prior Newtonsoft serialization behavior (e.g., omit empty collections).
  • Updates core pipeline/plugins/controllers/jobs to use ITextSerializer/JsonSerializerOptions instead of Newtonsoft abstractions.
  • Refactors tests and fixtures to validate STJ semantics (including semantic JSON comparison).

Reviewed changes

Copilot reviewed 87 out of 87 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Exceptionless.Tests/Utility/DataBuilder.cs Updates test builder to use ITextSerializer for manual stacking info.
tests/Exceptionless.Tests/Serializer/SerializerTests.cs Reworks serializer tests away from Newtonsoft-specific behavior toward STJ round-trips.
tests/Exceptionless.Tests/Serializer/Models/PersistentEventSerializerTests.cs Uses ITextSerializer for typed data retrieval in PersistentEvent tests.
tests/Exceptionless.Tests/Serializer/Models/DataDictionaryTests.cs Updates GetValue<T> tests to use ITextSerializer pathway.
tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs Compares serialized JSON via ITextSerializer instead of ToJson().
tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs Updates Slack token access to use serializer-aware accessor.
tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs Uses serializer-based typed accessors in repository tests.
tests/Exceptionless.Tests/Plugins/WebHookDataTests.cs Switches expected/actual comparisons to semantic JSON equivalence using STJ.
tests/Exceptionless.Tests/Plugins/SummaryDataTests.cs Moves summary comparisons to semantic JSON using STJ.
tests/Exceptionless.Tests/Plugins/GeoTests.cs Updates Geo plugin/tests to pass ITextSerializer.
tests/Exceptionless.Tests/Plugins/EventUpgraderTests.cs Uses JsonNode formatting + semantic compare for upgrader outputs.
tests/Exceptionless.Tests/Plugins/EventParserTests.cs Uses ITextSerializer for round-trip verification; removes Newtonsoft formatting asserts.
tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs Updates pipeline tests to use serializer-based typed accessors.
tests/Exceptionless.Tests/Mail/MailerTests.cs Updates Mailer construction to accept ITextSerializer.
tests/Exceptionless.Tests/IntegrationTestsBase.cs Replaces FluentRest Newtonsoft serializer with STJ JsonContentSerializer.
tests/Exceptionless.Tests/Exceptionless.Tests.csproj Removes FluentRest.NewtonsoftJson, adds STJ-based FluentRest.
tests/Exceptionless.Tests/Controllers/EventControllerTests.cs Uses ITextSerializer for typed accessors in controller tests.
src/Exceptionless.Web/Exceptionless.Web.csproj Removes NEST.JsonNetSerializer package reference.
src/Exceptionless.Web/Controllers/ProjectController.cs Injects ITextSerializer for Slack token access.
src/Exceptionless.Web/Controllers/EventController.cs Injects ITextSerializer and uses it for event payload byte generation.
src/Exceptionless.Core/Utility/TypeHelper.cs Updates equality handling for STJ JsonElement.
src/Exceptionless.Core/Utility/ExtensibleObject.cs Adds JsonElement handling to generic property retrieval.
src/Exceptionless.Core/Utility/ErrorSignature.cs Switches error signature extraction to serializer-based GetValue<T>.
src/Exceptionless.Core/Services/SlackService.cs Uses serializer-aware Slack token access.
src/Exceptionless.Core/Serialization/ObjectToInferredTypesConverter.cs Refines number inference logic for STJ dynamic object conversion.
src/Exceptionless.Core/Serialization/LowerCaseUnderscorePropertyNamesContractResolver.cs Deletes Newtonsoft contract resolver (obsolete after STJ migration).
src/Exceptionless.Core/Serialization/JsonSerializerOptionsExtensions.cs Defines Exceptionless STJ defaults (snake_case, safe encoder, converters, empty-collection skipping).
src/Exceptionless.Core/Serialization/ExceptionlessNamingStrategy.cs Deletes Newtonsoft naming strategy (replaced by STJ naming policy).
src/Exceptionless.Core/Serialization/EmptyCollectionModifier.cs Adds STJ type info modifier to omit empty collections.
src/Exceptionless.Core/Serialization/ElasticSystemTextJsonSerializer.cs Adds STJ IElasticsearchSerializer for NEST with custom converters.
src/Exceptionless.Core/Serialization/ElasticJsonNetSerializer.cs Deletes Newtonsoft-based NEST serializer.
src/Exceptionless.Core/Serialization/ElasticConnectionSettingsAwareContractResolver.cs Deletes Newtonsoft resolver used by old NEST serializer.
src/Exceptionless.Core/Serialization/DynamicTypeContractResolver.cs Deletes Newtonsoft dynamic contract resolver.
src/Exceptionless.Core/Serialization/DataObjectConverter.cs Deletes Newtonsoft-based model/data converter.
src/Exceptionless.Core/Repositories/Configuration/ExceptionlessElasticConfiguration.cs Switches Elasticsearch client wiring to STJ serializer.
src/Exceptionless.Core/Plugins/WebHook/Default/010_VersionOnePlugin.cs Uses serializer-based typed accessors; adds [JsonPropertyName] for v1 webhook compatibility.
src/Exceptionless.Core/Plugins/WebHook/Default/005_SlackPlugin.cs Uses serializer-based typed accessors for Slack webhook creation.
src/Exceptionless.Core/Plugins/Formatting/FormattingPluginBase.cs Converts formatting plugins to depend on ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/99_DefaultFormattingPlugin.cs Updates default formatting/slack attachment creation to use ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/60_LogFormattingPlugin.cs Updates typed accessors + slack attachment creation to use ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/50_SessionFormattingPlugin.cs Updates typed accessors to use ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/40_UsageFormattingPlugin.cs Updates typed accessors + slack attachment creation to use ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/30_NotFoundFormattingPlugin.cs Updates typed accessors + IP retrieval to use ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/20_ErrorFormattingPlugin.cs Updates typed accessors + slack attachment creation to use ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/10_SimpleErrorFormattingPlugin.cs Updates typed accessors + slack attachment creation to use ITextSerializer.
src/Exceptionless.Core/Plugins/Formatting/Default/05_ManualStackingFormattingPlugin.cs Updates manual stacking info access to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderContext.cs Migrates upgrader DOM from JObject/JArray to JsonObject/JsonArray.
src/Exceptionless.Core/Plugins/EventUpgrader/Default/V2_EventUpgrade.cs Rewrites upgrader logic from Newtonsoft DOM to JsonNode DOM.
src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R850_EventUpgrade.cs Rewrites upgrader logic to JsonObject.
src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R844_EventUpgrade.cs Rewrites upgrader logic to JsonObject.
src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R500_EventUpgrade.cs Rewrites upgrader logic to JsonObject and normalizes date string formatting.
src/Exceptionless.Core/Plugins/EventUpgrader/Default/GetVersion.cs Rewrites version detection to JsonNode APIs.
src/Exceptionless.Core/Plugins/EventProcessor/Default/90_RemovePrivateInformationPlugin.cs Updates typed accessors to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/80_AngularPlugin.cs Updates typed accessors to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/70_SessionPlugin.cs Updates typed accessors + session start creation to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/50_GeoPlugin.cs Updates IP extraction to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/45_EnvironmentInfoPlugin.cs Updates typed accessors to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/40_RequestInfoPlugin.cs Updates typed accessors and request exclusion processing to accept ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/30_SimpleErrorPlugin.cs Updates typed accessors to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/20_ErrorPlugin.cs Updates typed accessors + ErrorSignature to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/10_NotFoundPlugin.cs Updates typed accessors to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/0_ThrottleBotsPlugin.cs Updates request-info access to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventProcessor/Default/03_ManualStackingPlugin.cs Updates manual stacking info access to use ITextSerializer.
src/Exceptionless.Core/Plugins/EventParser/Default/LegacyErrorParserPlugin.cs Migrates legacy parsing to JsonNode + STJ options.
src/Exceptionless.Core/Plugins/EventParser/Default/JsonEventParserPlugin.cs Uses ITextSerializer for parsing event payloads instead of Newtonsoft.
src/Exceptionless.Core/Models/SummaryData.cs Adjusts nullability/required-ness to align with STJ behaviors.
src/Exceptionless.Core/Models/StackSummaryModel.cs Adjusts nullability/required-ness to align with STJ behaviors.
src/Exceptionless.Core/Models/Stack.cs Removes Newtonsoft enum converter attribute; keeps STJ converter.
src/Exceptionless.Core/Models/SlackToken.cs Replaces Newtonsoft [JsonProperty] with STJ [JsonPropertyName]; updates SlackAttachment ctor to use ITextSerializer.
src/Exceptionless.Core/Models/Messaging/ReleaseNotification.cs Removes required modifiers (likely to avoid STJ required-member enforcement).
src/Exceptionless.Core/Models/Event.cs Adds [JsonExtensionData] + IJsonOnDeserialized merge into Data.
src/Exceptionless.Core/Mail/Mailer.cs Switches user-info extraction to serializer-based accessors.
src/Exceptionless.Core/Jobs/WebHooksJob.cs Switches webhook POST payload handling to STJ PostAsJsonAsync + options.
src/Exceptionless.Core/Jobs/EventPostsJob.cs Uses serializer-based event bytes for retries.
src/Exceptionless.Core/Jobs/EventNotificationsJob.cs Updates request-info access to use ITextSerializer.
src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs Updates identity extraction to use ITextSerializer.
src/Exceptionless.Core/Extensions/RequestInfoExtensions.cs Requires serializer for post-data exclusion parsing (STJ).
src/Exceptionless.Core/Extensions/ProjectExtensions.cs Makes Slack token extraction serializer-aware via GetValue<T>.
src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs Updates helpers to accept ITextSerializer for typed accessors.
src/Exceptionless.Core/Extensions/JsonNodeExtensions.cs Adds STJ JsonNode helpers used by upgraders/tests (formatting, mutation helpers).
src/Exceptionless.Core/Extensions/JsonExtensions.cs Removes Newtonsoft helpers, retains JSON-type detection helpers.
src/Exceptionless.Core/Extensions/EventExtensions.cs Updates typed accessor APIs to accept ITextSerializer; switches GetBytes to serializer bytes.
src/Exceptionless.Core/Extensions/ErrorExtensions.cs Updates stacking target helper to use serializer-based error access.
src/Exceptionless.Core/Extensions/DataDictionaryExtensions.cs Reworks GetValue<T> around STJ/ITextSerializer, adds case-insensitive JsonElement handling.
src/Exceptionless.Core/Exceptionless.Core.csproj Removes Newtonsoft/JsonNet packages.
src/Exceptionless.Core/Bootstrapper.cs Removes Newtonsoft setup; registers STJ options + SystemTextJsonSerializer in DI.
AGENTS.md Documents the new STJ-only serialization architecture and security posture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Exceptionless.Core/Serialization/ElasticSystemTextJsonSerializer.cs Outdated
Comment thread src/Exceptionless.Core/Serialization/ElasticSystemTextJsonSerializer.cs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 87 out of 87 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Exceptionless.Core/Serialization/ElasticSystemTextJsonSerializer.cs Outdated
Comment thread src/Exceptionless.Core/Serialization/ObjectToInferredTypesConverter.cs Outdated
Comment thread src/Exceptionless.Core/Extensions/JsonNodeExtensions.cs Fixed
Comment thread src/Exceptionless.Core/Extensions/JsonNodeExtensions.cs Fixed
Comment thread src/Exceptionless.Core/Extensions/DataDictionaryExtensions.cs Fixed
@niemyjski niemyjski force-pushed the feature/system-text-json-v2 branch from e6a3b35 to c292e13 Compare March 1, 2026 00:52
Comment thread src/Exceptionless.Core/Extensions/JsonNodeExtensions.cs Fixed
Comment thread src/Exceptionless.Core/Extensions/JsonNodeExtensions.cs Fixed
Comment thread src/Exceptionless.Core/Extensions/JsonNodeExtensions.cs Fixed
Comment thread src/Exceptionless.Core/Extensions/ProjectExtensions.cs Fixed
@niemyjski niemyjski force-pushed the feature/system-text-json-v2 branch 2 times, most recently from 4d880ad to 09134ce Compare March 1, 2026 01:19
Copilot AI review requested due to automatic review settings March 22, 2026 00:51
@niemyjski niemyjski force-pushed the feature/system-text-json-v2 branch from 09134ce to 13aa277 Compare March 22, 2026 00:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 86 out of 86 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/Exceptionless.Tests/IntegrationTestsBase.cs Outdated
Comment thread tests/Exceptionless.Tests/Exceptionless.Tests.csproj Outdated
Comment thread src/Exceptionless.Core/Exceptionless.Core.csproj Outdated
Comment thread src/Exceptionless.Core/Utility/TypeHelper.cs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 117 out of 117 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Exceptionless.Core/Repositories/OrganizationRepository.cs
Comment thread src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs Fixed
Copilot AI review requested due to automatic review settings March 22, 2026 19:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 121 out of 121 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Exceptionless.Core/Repositories/OrganizationRepository.cs
Comment thread src/Exceptionless.Core/Serialization/EmptyCollectionModifier.cs Outdated
Comment thread tests/Exceptionless.Tests/IntegrationTestsBase.cs
Comment thread src/Exceptionless.Core/Utility/ExtensibleObject.cs Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 121 out of 121 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@niemyjski niemyjski requested a review from Copilot March 22, 2026 21:14
niemyjski added 29 commits May 21, 2026 10:44
Pass JsonSerializerOptions to SerializeToNode so UserInfo/UserDescription
nodes use snake_case property names instead of PascalCase defaults.

Also:
- Remove stale TryDeserializeWithFallback references in ProjectExtensions
- Add max depth protection to EventUpgraderContext JSON parsing
- Fix misleading enum storage comment in ES config (StackStatus is string)
- Update 977.expected.json for snake_case @user output
- Replace raw NEST ElasticFilter with typed Foundatio expressions
- Fix org abbreviation violations (org → organization)
- Simplify V2_EventUpgrade (remove unused options param)
- Update 977.expected.json for STJ format
- Add OrganizationRepository test coverage
- Add DictionaryKeyPolicy for typed object deserialization in GetValue
- Preserve user-provided keys when target is a dictionary type
- Remove synthetic 1477.snake-case.json (use original expected.json)
- Remove DeleteIndexesAsync hack from IntegrationTestsBase
- Simplify EventUpgraderContext (remove static parse options)
- Fix pipeline test assertions for snake_case keys (DictionaryKeyPolicy)
- Fix serializer test assertions for normalized dictionary keys
Covers all serialization paths, data flow lifecycle, GetValue<T>
dictionary extraction strategy, ObjectToInferredTypesConverter,
event upgrade pipeline, model annotations, ES divergences,
transitive dependencies, and production safety guarantees.
DictionaryKeyPolicy applied recursively to ALL dictionary keys at ALL
nesting levels, normalizing user-provided keys like 'SomeProp' to
'some_prop' in Error.Data, QueryString, etc. This is data corruption.

PropertyNamingPolicy=SnakeCaseLower alone handles typed C# property
name serialization. PropertyNameCaseInsensitive=true on the deserializer
handles snake_case to PascalCase property matching. Dictionary keys
(user data) don't need normalization.

Changes:
- Remove DictionaryKeyPolicy from s_dictSerializeOptions
- Remove IsDictionaryType helper and s_dictPreserveKeysOptions
- Revert corrupted test assertions back to original key casing
- Add snake-case fixture for WebHookDataTests (production ES format)
- Fix case-sensitivity test to match STJ SnakeCaseLower behavior
- Rename doc to serialization-architecture.md, remove DictionaryKeyPolicy docs
- Deleted 1477.snake-case.expected.json (should never have been created)
- WebHookDataTests now uses original 1477.expected.json fixture
- Added docs/foundatio-issues.md documenting 3 upstream issues

CRITICAL BUG FOUND: V1 webhook data extraction is broken after STJ migration.
GetError/GetRequestInfo/GetEnvironmentInfo not deserializing nested data correctly.
V1 webhook output missing MachineName, IpAddress, TargetMethod, query strings.
Needs investigation.
GetValue<T> failed to map multi-word PascalCase dictionary keys
(e.g., "ClientIpAddress", "MachineName", "TargetMethod") to their snake_case
typed property targets because STJ's PropertyNameCaseInsensitive only handles
case differences ("Message" ↔ "message") — it cannot structurally match
keys that differ by underscores ("ClientIpAddress" vs "client_ip_address").

This caused V1 webhooks to ship incomplete payloads (missing MachineName,
IpAddress, TargetMethod, full RequestPath) for any event whose @error,
@request, or @Environment data was stored with PascalCase keys (legacy V1
clients, pre-STJ test fixtures).

Fix: Add a recursive JsonNode key normalizer in DataDictionaryExtensions
that snake-cases dict keys ONLY at typed-property positions, walking into
typed collections and nested typed models. User-data dictionary positions
(Error.Data, RequestInfo.QueryString, RequestInfo.Cookies, etc.) are detected
via IDictionary<string, T> and left untouched — user-provided keys are
preserved exactly as submitted.

Added 6 targeted DataDictionary tests:
- PascalCase RequestInfo with multi-word keys (ClientIpAddress, etc.)
- PascalCase EnvironmentInfo with multi-word keys
- PascalCase Error with nested TargetMethod + StackFrame recursion
- PascalCase Error.Data user keys preserved exactly (SomeProp, MixedCASE_key)
- PascalCase RequestInfo.QueryString/Cookies keys preserved
- Mixed PascalCase + snake_case in same dictionary

Full suite: 1555/1557 pass (2 skipped are intentional perf tests).
…eserialization errors

Previously, all GetError/GetSimpleError/GetRequestInfo/GetEnvironmentInfo/
GetUserIdentity/GetSubmissionClient/GetLocation/GetManualStackingInfo/
GetUserDescription methods silently swallowed exceptions with `catch (Exception) { /* Ignored */ }`.

This meant deserialization failures (like the V1 webhook PascalCase bug just fixed)
were invisible in logs, making debugging nearly impossible.

Changes:
- Add optional `ILogger? logger = null` parameter to all 9 GetValue<T> extension methods
- Replace `catch (Exception)` with `catch (Exception ex)` and log as warnings with context
- Log format: "Failed to deserialize {DataKey} for event type {EventType}"
- Update webhook plugins (010_VersionOnePlugin, 005_SlackPlugin) to pass _logger
- Update event processor plugins (03_ManualStackingPlugin, 30_SimpleErrorPlugin,
  40_RequestInfoPlugin, 45_EnvironmentInfoPlugin, 90_RemovePrivateInformationPlugin) to pass _logger
- Other call sites (PersistentEventExtensions, ErrorExtensions, Mailer, FormattingPluginBase)
  continue to work without logger (optional parameter defaults to null)

Full suite: 1553/1555 pass, 2 skipped (intentional perf tests).
- Remove optional logger parameter (= null) from all Get* methods in EventExtensions
- Update all call sites across plugins, jobs, formatters, and tests to pass logger
- Add logger parameter to ToSessionStartEvent and GetIpAddresses extension methods
- Add logger parameter to SlackAttachment constructor
- Update Mailer.AddUserInfo to use instance logger instead of static method

All tests passing (1555/1557 passed, 2 skipped by design)
- Upgrade Foundatio.Repositories.Elasticsearch to 8.0.0-beta1.3 which fixes
  FieldValueHelper.ToFieldValue to respect [JsonStringEnumMemberName]
- Remove StackRepository workaround: use StackStatus.Open enum directly
  instead of string literal "open"
- Add _logger parameter to all Get* test call sites (required after previous commit)
- Add *.lscache to .gitignore, remove from tracking

EventRepository still uses ElasticFilter workaround for ExistsQuery on
dynamic fields (no upstream typed API available yet).
These regression tests were verified against origin/main before being added here. They pin the V1 webhook payload contract, legacy EnvironmentInfo OS fields, and large scientific-notation fallback behavior so the STJ branch cannot hide those differences.
- ObjectToInferredTypesConverter: Handle exponent numbers (e.g. 1e100) that
  overflow decimal range by falling back to double via TryGetDecimal
- DataDictionaryExtensions: Map both SnakeCaseLower(PropertyName) and
  JsonPropertyName attribute forms in GetPropertyMap, and normalize keys
  to the attribute name (e.g. 'o_s_name' not 'os_name') in NormalizeKeysForType
- V1 webhook fixtures: Remove null/empty fields to match STJ global options
  (WhenWritingNull + SkipEmptyCollections) — standard JSON convention,
  semantically equivalent for all webhook consumers
…exponents

RCA: The static ConvertJsonElementNumber helper (called from
Event.OnDeserialized for every extension data property) used unconditional
element.GetDecimal() for numbers with decimal points or exponents. Decimal
has max range ~±7.9×10²⁸, so values like 1e100 (common in telemetry data,
scientific measurements, and JS Number.MAX_VALUE) throw OverflowException.

The instance ReadNumber method already had the correct TryGetDecimal→double
fallback pattern but the static helper did not share this logic — an
asymmetry introduced when the static helper was extracted.

Fix: Use TryGetDecimal with GetDouble fallback in both the floating-point
branch AND the large-integer fallback, matching the instance method's
fault tolerance. Add direct unit tests for ConvertJsonElement with
out-of-decimal-range values.
RCA: JsonObject.Add throws ArgumentException if the key already exists.
The Rename and RenameOrRemoveIfNullOrEmpty methods rebuild the object in
property-order and substitute currentName with newName, but never checked
whether newName already existed as a separate property. When event upgrade
plugins rename a field to a name that malformed client data already
populated, this crashed the event pipeline.

Principle: Rename semantics should match Newtonsoft JObject behavior where
the last-written key wins. In a rename, the source value is the intentional
one (being migrated to the canonical name), so it takes precedence.

Fix: Skip properties whose key matches newName during the rebuild loop.
The renamed value is inserted at the original position, and the pre-existing
collision property is dropped. Both Rename and RenameOrRemoveIfNullOrEmpty
are patched consistently.
The Dict→JsonNode→String→T path in GetValue<T> performs three serialization
steps. The JsonNode intermediate is required for NormalizeKeysForType which
renames PascalCase keys in-place to match SnakeCaseLower property names.
The final ToJsonString→Deserialize could be eliminated by deserializing
directly from the node if ITextSerializer exposed JsonSerializerOptions.

Annotated for future optimization (not a correctness issue).
…behavior

The test 'Deserialize_JsonWithUnknownRootProperties_IgnoresUnknownProperties'
implied that unknown root properties are discarded. In reality, they are
captured via [JsonExtensionData] and merged into Event.Data by OnDeserialized().

Rename to 'MergesIntoData' and add assertions verifying the unknown properties
are actually present in ev.Data with correct values.
…paths

ASP.NET Core's DefaultProblemDetailsFactory adds 'traceId' (camelCase) to
ProblemDetails.Extensions. The DefaultProblemDetailsWriter (status code pages)
applies PropertyNamingPolicy to extension keys producing 'trace_id', but MVC's
SystemTextJsonOutputFormatter writes [JsonExtensionData] keys literally,
producing 'traceId'. This caused inconsistent API responses.

Fix: explicitly rename the key in CustomizeProblemDetails (called for both
paths) so the literal key is always 'trace_id'.

Also set ProblemDetails.Instance property directly instead of polluting the
Extensions dictionary — Instance is a first-class RFC 9457 member.
The @submission_client.version field reflects the FluentRest assembly version,
which was bumped from 11.0.0.0 to 11.1.0.0 as part of the dependency updates.
The PostEvent_WithExtraRootProperties_CapturedInDataBag and
PostEvent_WithExtraPropertiesAndKnownData_PreservesAllData tests used
conditional assertions (if ContainsKey) that would pass vacuously when
data was missing. Made assertions unconditional to properly validate
the serialization round-trip. Also added nested object/array
assertions in PostEvent_WithExtraComplexProperties_CapturedAsObjects
using 42L to match ES long integer behavior.
…lity

WebHooksJob now uses dedicated JsonSerializerOptions for webhook delivery
that does NOT use DefaultIgnoreCondition.WhenWritingNull or the
EmptyCollectionModifier. This ensures external webhook consumers continue
receiving null fields (DateFixed, Description, etc.) and empty collections
(Tags: []) in the payload — matching the original Newtonsoft.Json behavior.

The webhook test was also updated to use the same delivery-path options,
and the expected JSON files now reflect the full payload including nulls.
For v2 webhooks, this is an additive change (extra null fields) which is
safe for consumers that ignore unknown properties.
…Element

When TrySetPropertyValue receives a raw JsonElement (e.g., from
UnknownProperties in legacy PATCH endpoints), it was calling
JsonSerializer.Deserialize without the configured options. This would
fail for complex types like Dictionary<string, bool> that rely on the
snake_case naming policy. The DeltaJsonConverter now passes options to
the Delta instance for use in this fallback path.
…orruption

The STJ encoder uses UnicodeRanges.All which passes non-ASCII characters
through unescaped (unlike Newtonsoft which escaped to \\uXXXX). The existing
Encoding.ASCII.GetBytes would corrupt these characters (e.g., in release
notification messages). Also fixes the ArraySegment length parameter which
used string char count instead of byte array length.
The test scope naming used 'test' for id=0 and 'test-{N}' for id>0.
When Foundatio's DeleteIndexesAsync/ConfigureIndexesAsync ran with a
'test-*' wildcard, it inadvertently matched indices from 'test-1',
'test-2', etc., causing invalid_alias_name_exception failures.

Changed to 'xtest{N}' format so no scope prefix is a substring of
another, eliminating the race condition during parallel test execution.
totalOrphanedEventCount was declared and logged at the end of both
DeleteOrphanedEventsByProjectAsync and DeleteOrphanedEventsByOrganizationAsync
but never incremented inside the batch loop. Operators reading the
summary log line would always see 'Found 0 orphaned events' even when
hundreds were deleted. The deletes themselves were unaffected.

Same increment pattern already exists in DeleteOrphanedEventsByStackAsync.
The encoder was described as 'XSS-safe' that 'escapes <, >, &', but
JavaScriptEncoder.Create(UnicodeRanges.All) does the opposite — it
allows those characters through unescaped. The default encoder escapes
them. The actual intent is to allow non-ASCII Unicode (CJK, emoji, etc.)
through unescaped for readability. For JSON APIs the Content-Type header
is the XSS boundary, not the encoder.
Add 21 new tests across two test classes covering CleanupDataJob and
CleanupOrphanedDataJob with multi-tenant scenarios:

CleanupDataJobTests (9 new):
- Suspended token cleanup across multiple tenants
- Soft-deleted organization/project/stack cleanup isolation
- Retention period enforcement for free vs paid plans
- Combined multi-tenant full cleanup scenario

CleanupOrphanedDataJobTests (12 new):
- Orphaned events by stack/project/organization deletion
- Large volume test (5000 valid + 10000 orphaned events)
- Cross-tenant isolation verification
- Duplicate stack detection and cleanup
- Empty database edge case
- Combined all-orphan-types scenario

fix: preserve webhook backwards compatibility for V1 and V2 payloads

The old Newtonsoft code used DynamicTypeContractResolver which applied
different serialization rules per type:
- V1 (VersionOneWebHookEvent/Stack): default resolver — PascalCase via
  [JsonProperty], includes nulls and empty collections.
- V2 (WebHookEvent/Stack): LowerCaseUnderscorePropertyNamesContractResolver
  — snake_case naming, omits nulls and empty collections.

Replicate this in STJ with two options sets:
- V1: clone standard options + DefaultIgnoreCondition.Never +
  DefaultJsonTypeInfoResolver (no EmptyCollectionModifier)
- V2: clone standard options as-is (WhenWritingNull +
  EmptyCollectionModifier + SnakeCaseLower naming)

Pick options at serialization time based on the runtime type of the
webhook data object.

fix: correct misleading encoder comment — HTML chars ARE escaped

JavaScriptEncoder.Create(UnicodeRanges.All) allows non-ASCII through
but still escapes HTML-sensitive characters (<, >, &, ') to their
\\uXXXX forms. The old comment incorrectly claimed they were NOT escaped.
Test results confirm: & serializes as \\u0026.

fix: adapt test infrastructure for Elastic.Clients.Elasticsearch v8

The new Elastic.Clients.Elasticsearch v8 client differs from NEST in several
ways that broke test isolation:

- Does NOT split comma-separated index strings (DeleteByQuery targeted nothing)
- Concurrent index creation races during parallel test runs
- AppScope pool ID returned before host fully disposed (alias conflicts)
- ConfigureIndexesAsync cache prevented recreation after wildcard delete

Fix: use scope wildcard patterns for cleanup, add retry logic for alias races,
scope index configuration cache by ID, return pool IDs after full disposal,
and aggressively delete all indices for a scope on test init.

refactor: remove #region directives from test files

Regions add visual clutter without providing meaningful organization.
Test methods are already grouped by class and attribute-driven
discovery makes region-based grouping unnecessary.

Fix ArgumentException when multiple stacks share the same SignatureHash in cache

Dictionary.Add throws ArgumentException on duplicate keys. When multiple stacks
share the same SignatureHash (e.g. duplicate stacks created with same project/type/source),
AddDocumentsToCacheAsync would throw. Use indexer assignment instead so the last
write wins gracefully.

Fix WillExcludeOldStacksForStackNewMode test - give log events unique sources

Events 1, 2, and 3 all defaulted to Type=Log and Source='Test Event', giving them
identical DuplicateSignature values. The FixDuplicateStacks logic (run periodically)
merges stacks with the same DuplicateSignature, marking all but one as is_deleted=true.
This caused event 3's stack to be soft-deleted, returning only 1 result instead of 2.

Fix: assign unique Source values to each log event so they have distinct stacks
that won't be merged by the deduplication logic.

fix: DeltaJsonConverter respects [JsonPropertyName], removes itself from options copy; Delta uses TryGetValue

Add serialization audit tests for STJ migration verification

Comprehensive integration tests that submit events, webhooks, and patches
with different JSON property casings (snake_case, camelCase, PascalCase,
MIXED) and capture request/elastic/response JSON to audit-output/ for
cross-branch diffing.

Key findings documented via test output:
- Multi-word PascalCase/camelCase root properties (referenceId, ReferenceId)
  do not match snake_case wire names and fall into Data bag
- Single-word properties match case-insensitively with any casing
- NormalizeKeysForType in DataDictionaryExtensions correctly handles
  multi-word keys for typed models in the Data dictionary
- Character escaping: &→\\u0026, +→\\u002B, <→\\u003C, >→\\u003E in output
- Webhook creation requires snake_case property names (camelCase returns 400)

Add audit-output/ to .gitignore

Add casing compatibility and integration tests for STJ migration

Reproduce behavioral differences found during serialization audit:
- PascalCase/camelCase ReferenceId not binding (goes to ExtensionData)
- Date-only strings being parsed as DateTimeOffset
- Numeric precision, empty collection omission edge cases

These tests confirm the bugs before fixes are applied.

Fix PascalCase/camelCase ReferenceId binding in STJ deserialization

STJ's PropertyNameCaseInsensitive with SnakeCaseLower policy only matches
case-insensitively against the policy-transformed name (reference_id), so
PascalCase (ReferenceId) and camelCase (referenceId) end up in ExtensionData.

Add fallback in OnDeserialized to check ExtensionData for these alternate
casings and bind to the ReferenceId property, preserving backwards
compatibility with payloads submitted by older SDKs.

Preserve date-only strings instead of parsing as DateTimeOffset

Date-only strings like '2026-01-15' should not be expanded to
DateTimeOffset values (e.g., '2026-01-15T00:00:00-06:00') because:
1. They may not represent dates in user data
2. The timezone expansion adds information that wasn't in the original
3. This matches the legacy Newtonsoft behavior (DateParseHandling.None)

Only parse strings as dates when they contain a 'T' time separator,
indicating an explicit ISO 8601 datetime with time component.

Add serialization-audit skill for branch comparison workflow

Documents the workflow for generating, diffing, and testing serialization
behavior across branches during serializer migrations.

Clean up STJ migration: fix date parsing, add empty collection tests, document ReferenceId

- Remove lowercase 't' check in date parsing (ISO 8601 uses uppercase T only)
- Add tests for empty collection omission (tags, references, roundtrip)
- Remove decorative comment borders from test/script files
- Fix serialization-audit skill YAML frontmatter for discoverability
- Document ReferenceId fallback with SDK version context
…ewIndex to new ES client, fix validator types
Standardize DateTimeOffset parsing for robustness across locales by explicitly
using InvariantCulture and no special DateTimeStyles. This prevents unintended
date conversions.

Refine API data contracts by removing nullable annotations from array
properties in several record types (e.g., Elasticsearch, Migrations).
These arrays are now guaranteed to be non-null (though potentially empty),
simplifying consumption. Corresponding tests are updated to reflect this.

Includes minor code style updates for null checks (`is not null`) and
removes a redundant test assertion.

Fix RandomEventGeneratorTests: use properly configured serializer from shared static instance

Instead of 'new SystemTextJsonSerializer()' with default options (missing snake_case,
converters, etc.), use a shared static instance configured with ConfigureExceptionlessDefaults().
This matches the DI-registered serializer behavior without requiring full DI infrastructure
for this standalone unit test.

Add SetError/SetSimpleError extensions and use them in plugins

Adds SetError(Error) and SetSimpleError(SimpleError) extension methods
to EventExtensions, matching the existing GetError/GetSimpleError pattern.
Updates ErrorPlugin, SimpleErrorPlugin, and AngularPlugin to use these
extensions instead of direct dictionary assignment.

Fix critical data correctness issues

- OrganizationRepository: Fix suspended query logic — wrap FieldEquals
  conditions in FieldOr inside FieldNot so NOT(Active|Trialing|Canceled)
  works correctly instead of always-true NOT(AND of mutually exclusive)
- ObjectToInferredTypesConverter: Replace ToDictionary (throws on duplicate
  keys) with a loop using dict[key]= (last-wins) to prevent DoS via
  crafted JSON with duplicate object keys
- DataDictionaryExtensions: Rename s_ prefix to _ per project convention

Code quality improvements from PR review

- WebHooksJob: Remove redundant V2 options copy, use DI options directly
- WebHookRepository: Restore logging in MarkDisabledAsync, spell out
  'organization' per AGENTS.md convention
- ProjectExtensions: Use TryGetValue instead of ContainsKey+indexer,
  remove useless <remarks> tag
- 010_VersionOnePlugin: Use TryGetValue for SignatureInfo lookups
- CleanupOrphanedDataJob: Use FieldValueHelper.ToFieldValue for
  consistency with StackQuery pattern

Minor fixes: explicit byte[] type, restore comments, fix .gitignore

- WebSocketConnectionManager: Use explicit byte[] instead of var
- Startup.cs: Restore removed comments about serializer/ExternalAuthInfo
- .gitignore: Remove extra blank line

Test cleanup: remove bare AAA comments, fix prefixes, formatting

- Remove bare '// Arrange', '// Act', '// Assert' comments across all
  PR test files (kept contextual ones like '// Arrange - Org 1 is...')
- AppWebHostFactory: Rename s_ prefix to _ per convention
- SerializationAuditTests: Rename s_ prefix to _
- EventControllerTests: Remove duplicate assert, add blank lines
- WebHookDataTests: Remove redundant V2 options, use DI options
- Webhook expected JSON: Revert escaped \\u0026 back to &

Revert 'Test cleanup: remove bare AAA comments' - user wants AAA comments

This reverts commit 9400674ff. The user wants AAA comments (Arrange/Act/Assert)
added to tests where missing, not removed. Restoring them now.

Fix serializer instance creation and formatting per user feedback

- RandomEventGeneratorTests: Remove static serializer field, create inline
- ObjectToInferredTypesConverter: Add blank line before return dict

Document DataDictionaryExtensions _dictSerializeOptions rationale

Clarify that _dictSerializeOptions is only for intermediate Dict→JsonNode
conversion during key normalization, NOT for final deserialization which
uses ITextSerializer from DI. Addresses PR feedback about custom options.

Remove accidental frontend and Organization.cs changes from PR

These files got pulled in during the rebase conflict resolution and don't
belong in the STJ migration PR. Restored to main's version.

Address PR feedback: test naming, AAA comments, null safety, EmptyCollectionModifier

- Rename 4-part test names to 3-part (Method_Condition_Expected)
- Rename SerializationAuditTests 2-part names to 3-part
- Add AAA comments to all CasingCompatibilityTests
- Fix nullable dereference: use Assert.IsType instead of as-cast
- Add date assertion back to EventControllerTests
- Remove custom JsonSerializerOptions in EventUpgraderTests assert message
- Rename _options to _innerOptions in DeltaJsonConverter for clarity
- Fix EmptyCollectionModifier: null no longer treated as empty, remove
  IEnumerable MoveNext() side-effect risk (restrict to ICollection)
- Fix XML doc comment with unescaped angle brackets

ExtensibleObject.GetProperty<T>: accept JsonSerializerOptions for proper deserialization

Callers with complex types can pass configured options from DI.
Existing string callers work without changes (backward-compatible default null).

Fix RandomEventGeneratorTests: get serializer from DI instead of manual construction

Extends TestWithServices to resolve ITextSerializer via DI container.
Eliminates the last 'new SystemTextJsonSerializer(new JsonSerializerOptions())' usage.

Assert.NotNull(ev);
Assert.NotNull(ev.Data);
Assert.True(ev.Data.ContainsKey("date_only"));

// Verify complex properties are captured in Data via JsonExtensionData + OnDeserialized
Assert.NotNull(ev.Data);
Assert.True(ev.Data.ContainsKey("metadata"), "metadata key should be captured in Data");
// Verify complex properties are captured in Data via JsonExtensionData + OnDeserialized
Assert.NotNull(ev.Data);
Assert.True(ev.Data.ContainsKey("metadata"), "metadata key should be captured in Data");
Assert.True(ev.Data.ContainsKey("tags_list"), "tags_list key should be captured in Data");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants