Skip to content

Serializer: Fixes unsafe stream cast in FromStream<T>#5651

Merged
kirankumarkolli merged 7 commits intomasterfrom
users/nalutripician/fix-fromstream-cast-5620
Mar 11, 2026
Merged

Serializer: Fixes unsafe stream cast in FromStream<T>#5651
kirankumarkolli merged 7 commits intomasterfrom
users/nalutripician/fix-fromstream-cast-5620

Conversation

@NaluTripician
Copy link
Copy Markdown
Contributor

Description

Fixes #5620

Replaces the unsafe (T)(object)stream cast pattern with safe is pattern matching in all FromStream<T> serializer implementations across the SDK.

Problem

The FromStream<T> method in multiple serializer implementations uses the following pattern:

if (typeof(Stream).IsAssignableFrom(typeof(T)))
{
    return (T)(object)stream;
}

typeof(Stream).IsAssignableFrom(typeof(T)) returns true when T is Stream or any subclass (e.g., MemoryStream, FileStream). If T is a specific Stream subclass but the runtime stream parameter is a different Stream type, the cast (T)(object)stream throws a raw InvalidCastException with no context about what went wrong.

Example that throws:

// T = MemoryStream, but stream is actually a FileStream at runtime
serializer.FromStream<MemoryStream>(someFileStream); // InvalidCastException!

Fix

Replaced the unsafe cast with safe is pattern matching:

if (typeof(Stream).IsAssignableFrom(typeof(T)))
{
    if (stream is T typedStream)
    {
        return typedStream;
    }

    throw new InvalidCastException(
        $"Stream of type '{stream.GetType().FullName}' is not compatible "
        + $"with the requested type '{typeof(T).FullName}'.");
}

This provides:

  • ✅ Safe runtime type checking (no unexpected InvalidCastException)
  • ✅ A descriptive error message identifying both the actual and expected types
  • ✅ No behavioral change for the common case (T = Stream)

Files Changed

Core SDK Serializers (2):

  • Microsoft.Azure.Cosmos/src/Serializer/CosmosJsonDotNetSerializer.cs
  • Microsoft.Azure.Cosmos/src/Serializer/CosmosSystemTextJsonSerializer.cs

Sample Code (3):

  • Microsoft.Azure.Cosmos.Samples/Usage/SystemTextJson/CosmosSystemTextJsonSerializer.cs
  • Microsoft.Azure.Cosmos.Samples/Usage/ReEncryption/ReEncryptionSupport/ReEncryptionJsonSerializer.cs
  • Microsoft.Azure.Cosmos.Samples/Usage/ItemManagement/Program.cs

Encryption Modules (2):

  • Microsoft.Azure.Cosmos.Encryption/src/CosmosJsonDotNetSerializer.cs
  • Microsoft.Azure.Cosmos.Encryption.Custom/src/Common/CosmosJsonDotNetSerializer.cs

Unit Tests (2):

  • Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosJsonSerializerUnitTests.cs — 2 new tests
  • Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Json/CosmosSystemTextJsonSerializerTest.cs — 2 new tests

Not Changed

  • PatchOperationCore{T}.cs — Has a similar-looking pattern but casts FROM T TO Stream (upcast), which always succeeds. No fix needed.
  • Test utility serializers — Internal test code, not customer-facing.

Testing

  • 4 new unit tests added (2 per serializer):
    • ValidateFromStreamWithBaseStreamType / TestFromStreamWithBaseStreamType — Confirms FromStream<Stream>(memoryStream) succeeds (regression test)
    • ValidateFromStreamWithIncompatibleStreamTypeThrowsDescriptiveError / TestFromStreamWithIncompatibleStreamTypeThrowsDescriptiveError — Confirms FromStream<FileStream>(memoryStream) throws InvalidCastException with a descriptive message containing both type names
  • All existing tests continue to pass

Replaces the unsafe (T)(object)stream cast pattern with safe 'is' pattern
matching in all FromStream<T> implementations. The old pattern could throw
an InvalidCastException when T was a Stream subclass (e.g., MemoryStream)
but the runtime stream was a different Stream type.

The fix uses 'stream is T typedStream' for safe runtime type checking and
throws a descriptive InvalidCastException when the types are incompatible.

Fixed in 7 files (2 core SDK serializers, 3 samples, 2 encryption modules).
Added unit tests for both CosmosJsonDotNetSerializer and
CosmosSystemTextJsonSerializer confirming the fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NaluTripician NaluTripician changed the title [Internal] Serializer: Fixes unsafe stream cast in FromStream<T> Serializer: Fixes unsafe stream cast in FromStream<T> Mar 3, 2026
@NaluTripician NaluTripician self-assigned this Mar 3, 2026
Comment thread Microsoft.Azure.Cosmos.Encryption.Custom/src/Common/CosmosJsonDotNetSerializer.cs Outdated
Comment thread Microsoft.Azure.Cosmos.Encryption.Custom/src/Common/CosmosJsonDotNetSerializer.cs Outdated
NaluTripician and others added 2 commits March 4, 2026 11:39
Replaces the 3-part block (typeof guard + is check + throw) with
a simple 'if (stream is T typedStream)' pattern match across all
serializer files. Removes incompatible stream type tests since
mismatched cases now fall through to deserialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

@NaluTripician NaluTripician left a comment

Choose a reason for hiding this comment

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

Addressed both comments — simplified to just if (stream is T typedStream) { return typedStream; } across all files.

Re: 'Is above if still needed?' — Good call, the outer typeof(Stream).IsAssignableFrom(typeof(T)) guard is no longer needed. The stream is T pattern match already handles the type check at runtime.

Re: 'Isn't it supposed to fallback to the copy?' — You're right. Removed the throw and let incompatible cases fall through to the normal deserialization path. The simplified stream is T check handles both concerns cleanly.

kirankumarkolli
kirankumarkolli previously approved these changes Mar 5, 2026
…ypes

The simplified 'stream is T' check was too broad - when T=object (e.g. dynamic),
it always matched, returning the raw stream instead of deserializing. Restored
the typeof(Stream).IsAssignableFrom(typeof(T)) guard in a combined condition.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kirankumarkolli kirankumarkolli merged commit 326532a into master Mar 11, 2026
32 checks passed
@kirankumarkolli kirankumarkolli deleted the users/nalutripician/fix-fromstream-cast-5620 branch March 11, 2026 23:29
Copilot AI mentioned this pull request Mar 18, 2026
4 tasks
microsoft-github-policy-service Bot pushed a commit that referenced this pull request Mar 20, 2026
## Version Changes

| Property | Old | New |
|---|---|---|
| `ClientOfficialVersion` | 3.57.0 | **3.58.0** |
| `ClientPreviewVersion` | 3.58.0 | **3.59.0** |
| `ClientPreviewSuffixVersion` | preview.0 | **preview.0** |

## Changelog

### 3.59.0-preview.0 (Preview)

#### Added
- [#5502](#5502)
VectorIndex Policy: Adds Support for QuantizerType in IndexingPolicy
- [#5634](#5634)
Semantic Reranking: Adds response body in semantic reranking error
responses
- [#5685](#5685)
Read Consistency Strategy: Adds Read Consistency Strategy option for
read requests

### 3.58.0 (GA)

#### Added
- [#5447](#5447) Per
Partition Automatic Failover: Adds Hub Region Processing Only While
Routing Requests Failed with 404/1002 for single master accounts
- [#5551](#5551)
HPK: Adds internal CosmosClientOptions flag UseLengthAwareRangeComparer
for length aware range comparer rollout
- [#5582](#5582)
Query: Adds ability to choose global vs local/focused statistics for
FullTextScore
- [5610](#5610)
Refactors N-Region Synchronous Commit feature to use
IServiceConfigurationReaderVNext interface.
- [#5693](#5693)
ThinClient Integration: Adds Enable Multiple Http2 connection on
SocketsHttpHandler
- [#5614](#5614)
ThinClient Integration: Adds support for QueryPlan in thinclient mode

#### Fixed
- [#5597](#5597)
CosmosClient: Fixes ObjectDisposedException message when client is
disposed during request
- [#5613](#5613)
CrossRegionHedgingAvailabilityStrategy: Fixes ArgumentNullException race
condition in hedging cancellation
- [#5650](#5650)
Batch: Fixes null ErrorMessage when promoting status from MultiStatus
response
- [#5651](#5651)
Serializer: Fixes unsafe stream cast in FromStream<T>
- [#5697](#5697)
ResourceThrottleRetryPolicy: Fixes cumulativeRetryDelay tracking when
x-ms-retry-after-ms header is absent

### API Contract Diff (GA)

```diff
diff --git "a/Microsoft.Azure.Cosmos\\contracts\\API_3.57.0.txt" "b/Microsoft.Azure.Cosmos\\contracts\\API_3.58.0.txt"
index a1fa19e..1b74a69 100644
--- "a/Microsoft.Azure.Cosmos\\contracts\\API_3.57.0.txt"
+++ "b/Microsoft.Azure.Cosmos\\contracts\\API_3.58.0.txt"
@@ -639,6 +639,11 @@ namespace Microsoft.Azure.Cosmos
         public string DefaultLanguage { get; set; }
         public Collection<FullTextPath> FullTextPaths { get; set; }
     }
+    public enum FullTextScoreScope
+    {
+        Global = 0,
+        Local = 1,
+    }
     public sealed class GeospatialConfig
     {
         public GeospatialConfig();
@@ -869,6 +874,7 @@ namespace Microsoft.Azure.Cosmos
         public Nullable<bool> EnableLowPrecisionOrderBy { get; set; }
         public bool EnableOptimisticDirectExecution { get; set; }
         public Nullable<bool> EnableScanInQuery { get; set; }
+        public FullTextScoreScope FullTextScoreScope { get; set; }
         public Nullable<int> MaxBufferedItemCount { get; set; }
         public Nullable<int> MaxConcurrency { get; set; }
         public Nullable<int> MaxItemCount { get; set; }
```

### API Contract Diff (Preview)

```diff
diff --git "a/Microsoft.Azure.Cosmos\\contracts\\API_3.58.0-preview.0.txt" "b/Microsoft.Azure.Cosmos\\contracts\\API_3.59.0-preview.0.txt"
index af57dd8..1ae52c0 100644
--- "a/Microsoft.Azure.Cosmos\\contracts\\API_3.58.0-preview.0.txt"
+++ "b/Microsoft.Azure.Cosmos\\contracts\\API_3.59.0-preview.0.txt"
@@ -128,6 +128,7 @@ namespace Microsoft.Azure.Cosmos
         public new string IfMatchEtag { get; set; }
         public new string IfNoneMatchEtag { get; set; }
         public Nullable<int> PageSizeHint { get; set; }
+        public Nullable<ReadConsistencyStrategy> ReadConsistencyStrategy { get; set; }
     }
     public abstract class ChangeFeedStartFrom
     {
@@ -414,6 +415,7 @@ namespace Microsoft.Azure.Cosmos
         public Nullable<TimeSpan> OpenTcpConnectionTimeout { get; set; }
         public Nullable<PortReuseMode> PortReuseMode { get; set; }
         public Nullable<PriorityLevel> PriorityLevel { get; set; }
+        public Nullable<ReadConsistencyStrategy> ReadConsistencyStrategy { get; set; }
         public TimeSpan RequestTimeout { get; set; }
         public CosmosSerializer Serializer { get; set; }
         public CosmosSerializationOptions SerializerOptions { get; set; }
@@ -746,6 +748,11 @@ namespace Microsoft.Azure.Cosmos
         public string DefaultLanguage { get; set; }
         public Collection<FullTextPath> FullTextPaths { get; set; }
     }
+    public enum FullTextScoreScope
+    {
+        Global = 0,
+        Local = 1,
+    }
     public sealed class GeospatialConfig
     {
         public GeospatialConfig();
@@ -825,6 +832,7 @@ namespace Microsoft.Azure.Cosmos
         public Nullable<IndexingDirective> IndexingDirective { get; set; }
         public IEnumerable<string> PostTriggers { get; set; }
         public IEnumerable<string> PreTriggers { get; set; }
+        public Nullable<ReadConsistencyStrategy> ReadConsistencyStrategy { get; set; }
         public string SessionToken { get; set; }
     }
     public class ItemResponse<T> : Response<T>
@@ -972,6 +980,11 @@ namespace Microsoft.Azure.Cosmos
         High = 1,
         Low = 2,
     }
+    public enum QuantizerType
+    {
+        Product = 0,
+        Spherical = 1,
+    }
     public class QueryDefinition
     {
         public QueryDefinition(string query);
@@ -988,6 +1001,7 @@ namespace Microsoft.Azure.Cosmos
         public Nullable<bool> EnableLowPrecisionOrderBy { get; set; }
         public bool EnableOptimisticDirectExecution { get; set; }
         public Nullable<bool> EnableScanInQuery { get; set; }
+        public FullTextScoreScope FullTextScoreScope { get; set; }
         public Nullable<int> MaxBufferedItemCount { get; set; }
         public Nullable<int> MaxConcurrency { get; set; }
         public Nullable<int> MaxItemCount { get; set; }
@@ -995,6 +1009,7 @@ namespace Microsoft.Azure.Cosmos
         public Nullable<bool> PopulateIndexMetrics { get; set; }
         public Nullable<bool> PopulateQueryAdvice { get; set; }
         public QueryTextMode QueryTextMode { get; set; }
+        public Nullable<ReadConsistencyStrategy> ReadConsistencyStrategy { get; set; }
         public Nullable<int> ResponseContinuationTokenLimitInKb { get; set; }
         public string SessionToken { get; set; }
     }
@@ -1004,10 +1019,18 @@ namespace Microsoft.Azure.Cosmos
         None = 0,
         ParameterizedOnly = 1,
     }
+    public enum ReadConsistencyStrategy
+    {
+        Eventual = 1,
+        GlobalStrong = 4,
+        LatestCommitted = 3,
+        Session = 2,
+    }
     public class ReadManyRequestOptions : RequestOptions
     {
         public ReadManyRequestOptions();
         public Nullable<ConsistencyLevel> ConsistencyLevel { get; set; }
+        public Nullable<ReadConsistencyStrategy> ReadConsistencyStrategy { get; set; }
         public string SessionToken { get; set; }
     }
     public static class Regions
@@ -1383,6 +1406,7 @@ namespace Microsoft.Azure.Cosmos
         public int IndexingSearchListSize { get; set; }
         public string Path { get; set; }
         public int QuantizationByteSize { get; set; }
+        public Nullable<QuantizerType> QuantizerType { get; set; }
         public VectorIndexType Type { get; set; }
         public string[] VectorIndexShardKey { get; set; }
     }
@@ -1482,6 +1506,7 @@ namespace Microsoft.Azure.Cosmos.Fluent
         public CosmosClientBuilder WithHttpClientFactory(Func<HttpClient> httpClientFactory);
         public CosmosClientBuilder WithLimitToEndpoint(bool limitToEndpoint);
         public CosmosClientBuilder WithPriorityLevel(PriorityLevel priorityLevel);
+        public CosmosClientBuilder WithReadConsistencyStrategy(ReadConsistencyStrategy readConsistencyStrategy);
         public CosmosClientBuilder WithRequestTimeout(TimeSpan requestTimeout);
         public CosmosClientBuilder WithSerializerOptions(CosmosSerializationOptions cosmosSerializerOptions);
         public CosmosClientBuilder WithSystemTextJsonSerializerOptions(JsonSerializerOptions serializerOptions);
@@ -1540,6 +1565,7 @@ namespace Microsoft.Azure.Cosmos.Fluent
         public VectorIndexDefinition<T> Path(string path, VectorIndexType indexType);
         public VectorIndexDefinition<T> WithIndexingSearchListSize(int indexingSearchListSize);
         public VectorIndexDefinition<T> WithQuantizationByteSize(int quantizationByteSize);
+        public VectorIndexDefinition<T> WithQuantizerType(QuantizerType quantizerType);
         public VectorIndexDefinition<T> WithVectorIndexShardKey(string[] vectorIndexShardKey);
     }
 }
```

## Checklist
- [ ] Changelog review by team
- [ ] Email `azurecosmossdkdotnet@microsoft.com` for preview API review
- [ ] API contract diff approval (Kiran & Kirill)
- [ ] Kiran sign-off (required)
- [ ] Determine if "Recommended Version" needs further updating

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NaluTripician added a commit that referenced this pull request Mar 24, 2026
## Description

Fixes #5620

Replaces the unsafe `(T)(object)stream` cast pattern with safe `is`
pattern matching in all `FromStream<T>` serializer implementations
across the SDK.

### Problem

The `FromStream<T>` method in multiple serializer implementations uses
the following pattern:

```csharp
if (typeof(Stream).IsAssignableFrom(typeof(T)))
{
    return (T)(object)stream;
}
```

`typeof(Stream).IsAssignableFrom(typeof(T))` returns `true` when `T` is
`Stream` **or any subclass** (e.g., `MemoryStream`, `FileStream`). If
`T` is a specific `Stream` subclass but the runtime `stream` parameter
is a different `Stream` type, the cast `(T)(object)stream` throws a raw
`InvalidCastException` with no context about what went wrong.

**Example that throws:**
```csharp
// T = MemoryStream, but stream is actually a FileStream at runtime
serializer.FromStream<MemoryStream>(someFileStream); // InvalidCastException!
```

### Fix

Replaced the unsafe cast with safe `is` pattern matching:

```csharp
if (typeof(Stream).IsAssignableFrom(typeof(T)))
{
    if (stream is T typedStream)
    {
        return typedStream;
    }

    throw new InvalidCastException(
        $"Stream of type '{stream.GetType().FullName}' is not compatible "
        + $"with the requested type '{typeof(T).FullName}'.");
}
```

This provides:
- ✅ Safe runtime type checking (no unexpected `InvalidCastException`)
- ✅ A descriptive error message identifying both the actual and expected
types
- ✅ No behavioral change for the common case (`T = Stream`)

### Files Changed

**Core SDK Serializers (2):**
- `Microsoft.Azure.Cosmos/src/Serializer/CosmosJsonDotNetSerializer.cs`
-
`Microsoft.Azure.Cosmos/src/Serializer/CosmosSystemTextJsonSerializer.cs`

**Sample Code (3):**
-
`Microsoft.Azure.Cosmos.Samples/Usage/SystemTextJson/CosmosSystemTextJsonSerializer.cs`
-
`Microsoft.Azure.Cosmos.Samples/Usage/ReEncryption/ReEncryptionSupport/ReEncryptionJsonSerializer.cs`
- `Microsoft.Azure.Cosmos.Samples/Usage/ItemManagement/Program.cs`

**Encryption Modules (2):**
- `Microsoft.Azure.Cosmos.Encryption/src/CosmosJsonDotNetSerializer.cs`
-
`Microsoft.Azure.Cosmos.Encryption.Custom/src/Common/CosmosJsonDotNetSerializer.cs`

**Unit Tests (2):**
-
`Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosJsonSerializerUnitTests.cs`
— 2 new tests
-
`Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Json/CosmosSystemTextJsonSerializerTest.cs`
— 2 new tests

### Not Changed

- **`PatchOperationCore{T}.cs`** — Has a similar-looking pattern but
casts FROM `T` TO `Stream` (upcast), which always succeeds. No fix
needed.
- **Test utility serializers** — Internal test code, not
customer-facing.

### Testing

- 4 new unit tests added (2 per serializer):
- `ValidateFromStreamWithBaseStreamType` /
`TestFromStreamWithBaseStreamType` — Confirms
`FromStream<Stream>(memoryStream)` succeeds (regression test)
- `ValidateFromStreamWithIncompatibleStreamTypeThrowsDescriptiveError` /
`TestFromStreamWithIncompatibleStreamTypeThrowsDescriptiveError` —
Confirms `FromStream<FileStream>(memoryStream)` throws
`InvalidCastException` with a descriptive message containing both type
names
- All existing tests continue to pass

---------

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.

Cast may not always works in certain edge cases

2 participants