Skip to content

Commit 50bf76f

Browse files
Merge branch 'master' into copilot/fix-5154
2 parents b3e5f17 + 9edd18b commit 50bf76f

124 files changed

Lines changed: 16192 additions & 1453 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Build.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
22
<PropertyGroup>
3-
<ClientOfficialVersion>3.54.1</ClientOfficialVersion>
4-
<ClientPreviewVersion>3.55.0</ClientPreviewVersion>
5-
<ClientPreviewSuffixVersion>preview.1</ClientPreviewSuffixVersion>
6-
<DirectVersion>3.41.0</DirectVersion>
3+
<ClientOfficialVersion>3.56.0</ClientOfficialVersion>
4+
<ClientPreviewVersion>3.57.0</ClientPreviewVersion>
5+
<ClientPreviewSuffixVersion>preview.0</ClientPreviewSuffixVersion>
6+
<DirectVersion>3.41.2</DirectVersion>
77
<FaultInjectionVersion>1.0.0</FaultInjectionVersion>
88
<FaultInjectionSuffixVersion>beta.0</FaultInjectionSuffixVersion>
99
<EncryptionOfficialVersion>2.0.5</EncryptionOfficialVersion>

Exceptions.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,60 @@ Cosmos DB SDK on any IO failure will attempt to retry the failed operation if re
3333
## Common error status codes and troubleshooting guide <a id="error-codes"></a>
3434

3535
To see a list of common error code and issues please see [.NET SDK troubleshooting guide](https://docs.microsoft.com/azure/cosmos-db/troubleshoot-dot-net-sdk)
36+
37+
### Disambiguating 404 (Not Found) errors using SubStatusCode <a id="substatus-codes"></a>
38+
39+
When you receive a 404 (Not Found) status code from Cosmos DB, it can indicate two different scenarios:
40+
1. **Item not found**: The requested item doesn't exist in the container
41+
2. **Owner resource not found**: The parent resource (container or database) doesn't exist
42+
43+
To distinguish between these cases, check the `SubStatusCode` property:
44+
45+
- **SubStatusCode 0**: Regular item not found (the item doesn't exist in an existing container)
46+
- **SubStatusCode 1003**: Owner resource not found (the container or database doesn't exist)
47+
48+
#### Example with Typed APIs (throws CosmosException):
49+
50+
```csharp
51+
try
52+
{
53+
ItemResponse<MyItem> response = await container.ReadItemAsync<MyItem>("itemId", new PartitionKey("partitionKey"));
54+
}
55+
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
56+
{
57+
if (ex.SubStatusCode == 1003)
58+
{
59+
// The container or database doesn't exist
60+
Console.WriteLine("Owner resource (container/database) not found");
61+
}
62+
else
63+
{
64+
// The item doesn't exist in an existing container
65+
Console.WriteLine("Item not found");
66+
}
67+
}
68+
```
69+
70+
#### Example with Stream APIs (returns ResponseMessage):
71+
72+
```csharp
73+
ResponseMessage response = await container.ReadItemStreamAsync("itemId", new PartitionKey("partitionKey"));
74+
75+
if (response.StatusCode == HttpStatusCode.NotFound)
76+
{
77+
int subStatusCode = (int)response.Headers.SubStatusCode;
78+
79+
if (subStatusCode == 1003)
80+
{
81+
// The container or database doesn't exist
82+
Console.WriteLine("Owner resource (container/database) not found");
83+
}
84+
else
85+
{
86+
// The item doesn't exist in an existing container
87+
Console.WriteLine("Item not found");
88+
}
89+
}
90+
```
91+
92+
This distinction is particularly useful when implementing retry logic or error handling strategies, as you may want to handle these scenarios differently (e.g., creating the container if it doesn't exist vs. handling a missing item).

Microsoft.Azure.Cosmos.Encryption.Custom/src/AeadAes/SymmetricKey.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ internal class SymmetricKey
2323
/// <param name="rootKey">root key</param>
2424
internal SymmetricKey(byte[] rootKey)
2525
{
26-
// Key validation
27-
if (rootKey == null || rootKey.Length == 0)
26+
ArgumentValidation.ThrowIfNull(rootKey);
27+
28+
if (rootKey.Length == 0)
2829
{
29-
throw new ArgumentNullException(nameof(rootKey));
30+
throw new ArgumentException("The root key cannot be empty.", nameof(rootKey));
3031
}
3132

3233
this.rootKey = rootKey;

Microsoft.Azure.Cosmos.Encryption.Custom/src/Common/CosmosDiagnosticsContext.cs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,84 @@
55
namespace Microsoft.Azure.Cosmos.Encryption.Custom
66
{
77
using System;
8+
using System.Collections.Generic;
9+
using System.Diagnostics;
10+
using System.Threading;
811

912
/// <summary>
10-
/// This is an empty implementation of CosmosDiagnosticsContext which has been plumbed through the DataEncryptionKeyProvider and EncryptionContainer.
11-
/// This may help adding diagnostics more easily in future.
13+
/// Lightweight diagnostics context for Custom Encryption extension.
14+
/// Manages Activity creation for OpenTelemetry integration.
1215
/// </summary>
1316
internal class CosmosDiagnosticsContext
1417
{
15-
private static readonly CosmosDiagnosticsContext UnusedSingleton = new ();
16-
private static readonly IDisposable UnusedScopeSingleton = new Scope();
18+
private static readonly ActivitySource ActivitySource = new ("Microsoft.Azure.Cosmos.Encryption.Custom");
1719

20+
/// <summary>
21+
/// Scope name prefix for MDE (Microsoft.Data.Encryption) encrypt operations.
22+
/// </summary>
23+
internal const string ScopeEncryptModeSelectionPrefix = "EncryptionProcessor.Encrypt.Mde.";
24+
25+
/// <summary>
26+
/// Scope name prefix for MDE (Microsoft.Data.Encryption) decrypt operations.
27+
/// </summary>
28+
internal const string ScopeDecryptModeSelectionPrefix = "EncryptionProcessor.Decrypt.Mde.";
29+
30+
internal CosmosDiagnosticsContext()
31+
{
32+
}
33+
34+
/// <summary>
35+
/// Creates a new diagnostics context instance.
36+
/// </summary>
1837
public static CosmosDiagnosticsContext Create(RequestOptions options)
1938
{
2039
_ = options;
21-
return CosmosDiagnosticsContext.UnusedSingleton;
40+
return new CosmosDiagnosticsContext();
2241
}
2342

24-
public IDisposable CreateScope(string scope)
43+
/// <summary>
44+
/// Creates a new diagnostic scope for Activity tracking.
45+
/// </summary>
46+
/// <param name="scope">The name of the scope.</param>
47+
/// <returns>A <see cref="Scope"/> that manages an Activity lifecycle.</returns>
48+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="scope"/> is null.</exception>
49+
/// <exception cref="ArgumentException">Thrown when <paramref name="scope"/> is empty or whitespace.</exception>
50+
/// <remarks>
51+
/// Use with a <c>using</c> statement to ensure proper disposal.
52+
/// </remarks>
53+
public Scope CreateScope(string scope)
2554
{
26-
_ = scope;
27-
return CosmosDiagnosticsContext.UnusedScopeSingleton;
55+
ArgumentValidation.ThrowIfNullOrWhiteSpace(scope, nameof(scope));
56+
57+
Activity activity = ActivitySource.HasListeners() ? ActivitySource.StartActivity(scope, ActivityKind.Internal) : null;
58+
59+
return new Scope(activity);
2860
}
2961

30-
private class Scope : IDisposable
62+
/// <summary>
63+
/// Represents a diagnostic scope for Activity tracking.
64+
/// Must be used with the 'using' pattern to ensure proper disposal.
65+
/// </summary>
66+
/// <remarks>
67+
/// Dispose() is idempotent - calling it multiple times will only dispose the Activity once.
68+
/// </remarks>
69+
public sealed class Scope : IDisposable
3170
{
71+
private readonly Activity activity;
72+
private bool isDisposed;
73+
74+
internal Scope(Activity activity)
75+
{
76+
this.activity = activity;
77+
}
78+
3279
public void Dispose()
3380
{
81+
if (!this.isDisposed)
82+
{
83+
this.isDisposed = true;
84+
this.activity?.Dispose();
85+
}
3486
}
3587
}
3688
}

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,7 @@ internal CosmosJsonDotNetSerializer(JsonSerializerSettings jsonSerializerSetting
4040
/// <returns>The object representing the deserialized stream</returns>
4141
public T FromStream<T>(Stream stream)
4242
{
43-
#if NET8_0_OR_GREATER
44-
ArgumentNullException.ThrowIfNull(stream);
45-
#else
46-
if (stream == null)
47-
{
48-
throw new ArgumentNullException(nameof(stream));
49-
}
50-
#endif
43+
ArgumentValidation.ThrowIfNull(stream);
5144

5245
if (typeof(Stream).IsAssignableFrom(typeof(T)))
5346
{
@@ -87,6 +80,41 @@ public MemoryStream ToStream<T>(T input)
8780
return streamPayload;
8881
}
8982

83+
/// <summary>
84+
/// Serializes an object directly into the provided output stream (which remains open).
85+
/// </summary>
86+
/// <typeparam name="T">Type of object being serialized.</typeparam>
87+
/// <param name="input">Object to serialize.</param>
88+
/// <param name="output">Destination stream. Must be writable. The stream is not disposed by this method.</param>
89+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="output"/> is <c>null</c>.</exception>
90+
/// <exception cref="ArgumentException">Thrown when <paramref name="output"/> is not writable.</exception>
91+
/// <remarks>
92+
/// <para>This method serializes the object directly to the provided stream without creating an intermediate MemoryStream,
93+
/// reducing memory allocations for large objects.</para>
94+
/// <para>After writing, the stream position will be at the end of the written content.
95+
/// Callers are responsible for resetting the stream position if needed for subsequent reads.</para>
96+
/// </remarks>
97+
public void WriteToStream<T>(T input, Stream output)
98+
{
99+
ArgumentValidation.ThrowIfNull(output);
100+
101+
if (!output.CanWrite)
102+
{
103+
throw new ArgumentException("Output stream must be writable", nameof(output));
104+
}
105+
106+
using (StreamWriter streamWriter = new (output, encoding: CosmosJsonDotNetSerializer.DefaultEncoding, bufferSize: 1024, leaveOpen: true))
107+
using (JsonTextWriter writer = new (streamWriter))
108+
{
109+
writer.ArrayPool = JsonArrayPool.Instance;
110+
writer.Formatting = Newtonsoft.Json.Formatting.None;
111+
JsonSerializer jsonSerializer = this.GetSerializer();
112+
jsonSerializer.Serialize(writer, input);
113+
writer.Flush();
114+
streamWriter.Flush();
115+
}
116+
}
117+
90118
/// <summary>
91119
/// JsonSerializer has hit a race conditions with custom settings that cause null reference exception.
92120
/// To avoid the race condition a new JsonSerializer is created for each call
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
//------------------------------------------------------------
4+
5+
namespace Microsoft.Azure.Cosmos.Encryption.Custom
6+
{
7+
using System.IO;
8+
using System.Threading.Tasks;
9+
10+
/// <summary>
11+
/// Extension methods for Stream to provide compatibility across different .NET versions.
12+
/// </summary>
13+
internal static class StreamExtensions
14+
{
15+
/// <summary>
16+
/// Asynchronously disposes the stream in a version-compatible way.
17+
/// Uses DisposeAsync on .NET 8.0+ and falls back to synchronous Dispose on earlier versions.
18+
/// </summary>
19+
/// <param name="stream">The stream to dispose.</param>
20+
/// <returns>A ValueTask representing the asynchronous dispose operation.</returns>
21+
public static ValueTask DisposeCompatAsync(this Stream stream)
22+
{
23+
#if NET8_0_OR_GREATER
24+
return stream.DisposeAsync();
25+
#else
26+
stream.Dispose();
27+
return default;
28+
#endif
29+
}
30+
}
31+
}

Microsoft.Azure.Cosmos.Encryption.Custom/src/CosmosDataEncryptionKeyProvider.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,7 @@ public async Task InitializeAsync(
148148
throw new InvalidOperationException($"{nameof(CosmosDataEncryptionKeyProvider)} has already been initialized.");
149149
}
150150

151-
#if NET8_0_OR_GREATER
152-
ArgumentNullException.ThrowIfNull(database);
153-
#else
154-
if (database == null)
155-
{
156-
throw new ArgumentNullException(nameof(database));
157-
}
158-
#endif
151+
ArgumentValidation.ThrowIfNull(database);
159152

160153
ContainerResponse containerResponse = await database.CreateContainerIfNotExistsAsync(
161154
containerId,

Microsoft.Azure.Cosmos.Encryption.Custom/src/DataEncryptionKey.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,7 @@ public static DataEncryptionKey Create(
102102
byte[] rawKey,
103103
string encryptionAlgorithm)
104104
{
105-
#if NET8_0_OR_GREATER
106-
ArgumentNullException.ThrowIfNull(rawKey);
107-
#else
108-
if (rawKey == null)
109-
{
110-
throw new ArgumentNullException(nameof(rawKey));
111-
}
112-
#endif
105+
ArgumentValidation.ThrowIfNull(rawKey);
113106

114107
#pragma warning disable CS0618 // Type or member is obsolete
115108
if (!string.Equals(encryptionAlgorithm, CosmosEncryptionAlgorithm.AEAes256CbcHmacSha256Randomized))

Microsoft.Azure.Cosmos.Encryption.Custom/src/DataEncryptionKeyContainerCore.cs

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,14 @@ public override async Task<ItemResponse<DataEncryptionKeyProperties>> CreateData
5757
ItemRequestOptions requestOptions = null,
5858
CancellationToken cancellationToken = default)
5959
{
60-
if (string.IsNullOrEmpty(id))
61-
{
62-
throw new ArgumentNullException(nameof(id));
63-
}
60+
ArgumentValidation.ThrowIfNullOrEmpty(id);
6461

6562
if (!CosmosEncryptionAlgorithm.VerifyIfSupportedAlgorithm(encryptionAlgorithm))
6663
{
6764
throw new ArgumentException(string.Format("Unsupported Encryption Algorithm {0}", encryptionAlgorithm), nameof(encryptionAlgorithm));
6865
}
6966

70-
#if NET8_0_OR_GREATER
71-
ArgumentNullException.ThrowIfNull(encryptionKeyWrapMetadata);
72-
#else
73-
if (encryptionKeyWrapMetadata == null)
74-
{
75-
throw new ArgumentNullException(nameof(encryptionKeyWrapMetadata));
76-
}
77-
#endif
67+
ArgumentValidation.ThrowIfNull(encryptionKeyWrapMetadata);
7868

7969
CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions);
8070

@@ -159,14 +149,7 @@ public override async Task<ItemResponse<DataEncryptionKeyProperties>> RewrapData
159149
ItemRequestOptions requestOptions = null,
160150
CancellationToken cancellationToken = default)
161151
{
162-
#if NET8_0_OR_GREATER
163-
ArgumentNullException.ThrowIfNull(newWrapMetadata);
164-
#else
165-
if (newWrapMetadata == null)
166-
{
167-
throw new ArgumentNullException(nameof(newWrapMetadata));
168-
}
169-
#endif
152+
ArgumentValidation.ThrowIfNull(newWrapMetadata);
170153

171154
CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions);
172155

Microsoft.Azure.Cosmos.Encryption.Custom/src/DataEncryptionKeyContainerInlineCore.cs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,7 @@ public override Task<ItemResponse<DataEncryptionKeyProperties>> ReadDataEncrypti
5656
ItemRequestOptions requestOptions = null,
5757
CancellationToken cancellationToken = default)
5858
{
59-
if (string.IsNullOrEmpty(id))
60-
{
61-
throw new ArgumentNullException(nameof(id));
62-
}
59+
ArgumentValidation.ThrowIfNullOrEmpty(id);
6360

6461
return TaskHelper.RunInlineIfNeededAsync(() =>
6562
this.dataEncryptionKeyContainerCore.ReadDataEncryptionKeyAsync(id, requestOptions, cancellationToken));
@@ -73,19 +70,8 @@ public override Task<ItemResponse<DataEncryptionKeyProperties>> RewrapDataEncryp
7370
ItemRequestOptions requestOptions = null,
7471
CancellationToken cancellationToken = default)
7572
{
76-
if (string.IsNullOrEmpty(id))
77-
{
78-
throw new ArgumentNullException(nameof(id));
79-
}
80-
81-
#if NET8_0_OR_GREATER
82-
ArgumentNullException.ThrowIfNull(newWrapMetadata);
83-
#else
84-
if (newWrapMetadata == null)
85-
{
86-
throw new ArgumentNullException(nameof(newWrapMetadata));
87-
}
88-
#endif
73+
ArgumentValidation.ThrowIfNullOrEmpty(id);
74+
ArgumentValidation.ThrowIfNull(newWrapMetadata);
8975

9076
return TaskHelper.RunInlineIfNeededAsync(() =>
9177
this.dataEncryptionKeyContainerCore.RewrapDataEncryptionKeyAsync(id, newWrapMetadata, encryptionAlgorithm, requestOptions, cancellationToken));

0 commit comments

Comments
 (0)