Repository navigation
feat(json): add a converter that stores a TimeSpan as a number of ticks - #67
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
No blocking defects were identified; remaining feedback concerns minor coding conventions.
0 open findings
What changed in this PR
Adds an opt-in System.Text.Json converter that stores durations as numeric ticks for Cosmos DB sorting and filtering, while preserving reads of legacy text values.
Changes:
- Adds tick serialization, including nullable values and dictionary keys.
- Adds unit and emulator coverage with configurable JSON clients.
- Documents setup, migration, and precision limits.
| File | Description |
|---|---|
| tests/Cosmos.Tests/TimeSpanTicksConverterTests.fs | Tests serialization and supported inputs. |
| tests/Cosmos.Tests/TimeSpanTicksConverterIntegrationTests.fs | Tests sorting, filtering, and legacy reads. |
| tests/Cosmos.Tests/TestCategories.fs | Adds converter test categories. |
| tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj | Includes both test files. |
| tests/Cosmos.Tests.Infrastructure/Emulator.fs | Adds configurable JSON clients. |
| src/Cosmos/JsonConverters.fs | Implements the ticks converter. |
| src/Cosmos/FSharp.Azure.Cosmos.fsproj | Includes the converter. |
| docsSrc/How_Tos/Store_Durations.md | Explains usage and limitations. |
| CHANGELOG.md | Announces the converter. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
b346cf0 to
76d94c7
Compare
TimeSpan as a number of ticks
xperiandri
left a comment
There was a problem hiding this comment.
If you always seed the same data why don't you derive from OperationTestBase<> with scenario?
76d94c7 to
67082c4
Compare
|
About the review comment "derive from
The 31 |
67082c4 to
883b40e
Compare
af6b72f to
b268450
Compare
3806e97 to
048e40f
Compare
048e40f to
249478c
Compare
A scenario created its container in SeedDataAsync. The scenarios also repeated the identifier and the partition key path of the container of OperationTestBase. The review of #67 asked why the fixture does not make sure that all containers exist. Add the virtual member Containers to DatabaseTestApplicationFactory. A scenario overrides it to declare its containers. InitializeAsync creates them before SeedDataAsync runs. By default, ContainerCount is the number of declared containers, and at least one. Thus a test that creates its containers itself works as before. Add OperationTestApplicationFactory, the base class of the scenarios of the operation tests. It declares the container of OperationTestBase and gives it to a scenario as Container. SingleItemScenario and MultipleItemsScenario use it, and OperationTestBase uses its constants. Update the test-data-seeding skill and its examples. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
249478c to
63ab72d
Compare
A scenario created its container in SeedDataAsync. The scenarios also repeated the identifier and the partition key path of the container of OperationTestBase. The review of #67 asked why the fixture does not make sure that all containers exist. Add the virtual member Containers to DatabaseTestApplicationFactory. A scenario overrides it to declare its containers. InitializeAsync creates them before SeedDataAsync runs. By default, ContainerCount is the number of declared containers, and at least one. Thus a test that creates its containers itself works as before. Containers is a ContainerSet, a set of container definitions with the identifier as key. It throws when a scenario declares a container twice. CreateContainerIfNotExistsAsync would hide the second declaration. It returns the container that exists already and ignores all of its other settings, except the partition key path. Add OperationTestApplicationFactory, the base class of the scenarios of the operation tests. It declares the container of OperationTestBase and gives it to a scenario as Container. SingleItemScenario and MultipleItemsScenario use it, and OperationTestBase uses its constants. Update the test-data-seeding skill and its examples. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
63ab72d to
90b91d7
Compare
A scenario created its container in SeedDataAsync. The scenarios also repeated the identifier and the partition key path of the container of OperationTestBase. The review of #67 asked why the fixture does not make sure that all containers exist. Add the virtual member Containers to DatabaseTestApplicationFactory. A scenario overrides it to declare its containers. InitializeAsync creates them before SeedDataAsync runs. By default, ContainerCount is the number of declared containers, and at least one. Thus a test that creates its containers itself works as before. Containers is a ContainerSet, a set of container definitions with the identifier as key. It throws when a scenario declares a container twice. CreateContainerIfNotExistsAsync would hide the second declaration. It returns the container that exists already and ignores all of its other settings, except the partition key path. Add OperationTestApplicationFactory, the base class of the scenarios of the operation tests. It declares the container of OperationTestBase and gives it to a scenario as Container. SingleItemScenario and MultipleItemsScenario use it, and OperationTestBase uses its constants. Update the test-data-seeding skill and its examples. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
90b91d7 to
20267bb
Compare
20267bb to
9672633
Compare
Cosmos DB compares text by character code. System.Text.Json writes a TimeSpan as text. Thus "2.00:00:00" sorts after "10.00:00:00", and a query with ORDER BY returns the wrong order. System.Text.Json has no option for another format. Add TimeSpanTicksConverter. It writes the number of ticks. A number sorts and compares in time order. The converter reads the old text form too, so a container with old documents stays readable. It also handles Nullable<TimeSpan> and the key of a dictionary. Add 27 unit tests and 4 tests for the emulator. The emulator tests show the sort order, a filter with a TimeSpan parameter, and the read of documents in the old text form. A scenario writes the same durations through each serializer into a partition of its own. Add Emulator.createClientWithJson to the test infrastructure for a client with System.Text.Json options. Add the how-to page "How To store a duration" and a CHANGELOG entry. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9672633 to
e8b8229
Compare
Problem
Cosmos DB compares text by character code. System.Text.Json writes a
TimeSpanas text, for example"2.00:00:00". Thus a query withORDER BYor>on aTimeSpanfield returns the wrong result:"2.00:00:00"sorts after"10.00:00:00"."23:00:00"sorts after both.System.Text.Json has no option for another format.
JsonSerializerOptionshas no setting for it, and FSharp.SystemTextJson has no converter forTimeSpan.Change
TimeSpanTicksConverter(src/Cosmos/JsonConverters.fs). It writes the number of ticks as a JSON number. One tick is 100 nanoseconds. Cosmos DB sorts and compares a number in time order.Nullable<TimeSpan>and aTimeSpanas the key of a dictionary.How To store a durationand an entry inCHANGELOG.md.Emulator.createClientWithJsonto the test infrastructure. It creates a client with System.Text.Json options.The converter is for System.Text.Json. The library has no other dependency for it, because
net10.0includes System.Text.Json.Tests
TimeSpan, the old text form (also the text that the default converter writes), values that must fail,Nullable<TimeSpan>, dictionary keys, and a record.DurationsScenariowrites the same six durations two times, through each serializer into a partition of its own, and the tests read them. As the rule for scenarios says, the test class refers toDurationsScenario.TestApplicationFactorythrough the namespace and opens the namespace for the other types of the scenario. The scenario inherits fromOperationTestApplicationFactory(test: let the fixture create the containers of a scenario #71), so the fixture creates the container:ORDER BYreturns the durations in time order with the converter.TimeSpanparameter keeps the right durations. The client writes the parameter with the converter.ORDER BYreturns the order of the text. This is why the converter exists.On the Windows emulator (
Microsoft.Azure.Cosmos3.62.0), the 31TimeSpantests and the 14 tests of the categories "Read", "Read many" and "Iteration extensions" pass. Before the scenario, the whole suite passed (233 tests). The Release and Debug builds add no warning, and the strict documentation build passes.Notes for the reviewer
TimeSpan. The unit is not in the document, so a query written by hand must use ticks. The how-to page says so.DateTimeandDateTimeOffsethave a related problem: System.Text.Json drops the trailing zeros of the fraction, so...:00Zsorts after...:00.5Z. This PR does not change them. A converter for them fits in the same file.🤖 Generated with Claude Code