Skip to content
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
c54618a
Add JsonProcessor override via RequestOptions property bag
adamnova Sep 12, 2025
23749e0
Add diagnostics context and stream processor tests
adamnova Sep 12, 2025
22e40d7
Refactor diagnostics scopes in decryption logic
adamnova Sep 15, 2025
d6b3416
Optimize stream serialization and diagnostics in encryption
adamnova Sep 15, 2025
6e5884c
Refactor test encryptor setup and improve MDE fallback
adamnova Sep 16, 2025
585e66c
Unify JSON processor diagnostics and add concurrency tests
adamnova Sep 23, 2025
27d2593
Enforce object root for streaming encryption
adamnova Sep 23, 2025
8b84c66
Enforce object root for streaming encryption
adamnova Sep 24, 2025
78f0a94
Add comprehensive diagnostics scope tests for encryption
adamnova Sep 24, 2025
0812ac5
Refactor MDE encryption processor with adapter pattern
adamnova Sep 24, 2025
9850d4b
Refactor diagnostics scopes and add adapter tests
adamnova Sep 25, 2025
752905f
[Encryption Stream] Fix provided-output decrypt tests: ensure raw enc…
adamnova Sep 25, 2025
c09f71f
Add tests for Newtonsoft override and change feed assertions
adamnova Sep 25, 2025
9f8318b
Update diagnostics scope count assertions in tests
adamnova Sep 26, 2025
459c21e
Fixed tests
adamnova Sep 26, 2025
7b2170c
Update EncryptionBenchmark.cs
adamnova Sep 26, 2025
5aa3de0
Cleanup
adamnova Sep 26, 2025
4dcd1c8
Add experimental JSON processor configuration support
adamnova Sep 26, 2025
c4f6365
Update RequestOptionsOverrideHelper.cs
adamnova Sep 26, 2025
f11fbed
Cleanup conditional compilation
adamnova Sep 26, 2025
ec9b77e
Merge branch 'master' into feature/encryptionprocessor-stream-switch
adamnova Sep 26, 2025
3e7ce8a
Revert
adamnova Sep 26, 2025
c35ffcf
Cleanup
adamnova Sep 26, 2025
db67e7c
Cleanup
adamnova Sep 26, 2025
9496752
Changes
adamnova Sep 26, 2025
7f191e9
Improved naming
adamnova Sep 26, 2025
410f6ca
Refactor RequestOptions extension method usage
adamnova Sep 26, 2025
eb54301
Refactor decryption logic for streamlined processor selection
adamnova Sep 30, 2025
4f6c466
Cleanup preview flag
adamnova Sep 30, 2025
849f58e
Cleanup
adamnova Sep 30, 2025
33dc1e9
Cleanup
adamnova Sep 30, 2025
6ac8394
Cleanup
adamnova Sep 30, 2025
d5ea198
Reverted some changes and added documentation
adamnova Sep 30, 2025
c1cfb95
Refactor argument null checks to use ArgumentValidation
adamnova Sep 30, 2025
98a5310
Standardization
adamnova Oct 1, 2025
75d2684
Merge branch 'master' into feature/encryptionprocessor-stream-switch
adamnova Oct 6, 2025
1dbba14
Refactor encryption processor to use adapter pattern
adamnova Oct 8, 2025
565392c
Update conditional compilation for DecryptAsync methods
adamnova Oct 8, 2025
2bbf6e0
Update baseline tests and API contracts for Cosmos SDK
adamnova Oct 9, 2025
d76440d
Update baseline test for trace writer activities
adamnova Oct 9, 2025
3a4b05b
Delegate decryption to MdeEncryptionProcessor
adamnova Oct 10, 2025
aaba649
Revert "Delegate decryption to MdeEncryptionProcessor"
adamnova Oct 10, 2025
1bb97dd
Add support for legacy decryption in EncryptionProcessor
adamnova Oct 10, 2025
a070536
Refactor legacy decryption path and update tests
adamnova Oct 10, 2025
0d8ad47
Handle exceptions during legacy encryption detection
adamnova Oct 10, 2025
43e7804
Update API contract files and baseline test data
adamnova Oct 10, 2025
8e13f88
Merge branch 'master' into feature/encryptionprocessor-stream-switch
adamnova Oct 13, 2025
754997c
Remove ENCRYPTION_CUSTOM_PREVIEW conditional compilation
adamnova Oct 13, 2025
1c1f79c
Add NET8_0_OR_GREATER conditional compilation
adamnova Oct 13, 2025
e099ff0
Add experimental API and update trace baseline
adamnova Oct 13, 2025
e244861
Add test for unsupported JsonProcessor exception
adamnova Oct 14, 2025
a18b8f6
Merge branch 'master' into feature/encryptionprocessor-stream-switch
adamnova Oct 14, 2025
132b6e2
Merge branch 'master' into feature/encryptionprocessor-stream-switch
adamnova Oct 15, 2025
55b103b
Remove compression options from test encryption options
adamnova Oct 15, 2025
1421b3e
Merge branch 'master' into feature/encryptionprocessor-stream-switch
kirankumarkolli Oct 15, 2025
bc7e3ef
Rename StreamAdapter and related extensions for clarity
adamnova Oct 21, 2025
a486d40
Refactor null checks to use ArgumentValidation.ThrowIfNull
adamnova Oct 21, 2025
ce197c0
Make EncryptionRequestOptionsExperimental internal and remove experim…
adamnova Oct 21, 2025
aeeaf0c
Inline EncryptionRequestOptionsExperimental logic and remove unused c…
adamnova Oct 21, 2025
8e81b5e
Merge branch 'master' into feature/encryptionprocessor-stream-switch
adamnova Oct 30, 2025
76174f9
docs: Add comprehensive PR description and Kiran's feedback tracking
adamnova Oct 31, 2025
1ffd161
fix: Add defensive null check in CosmosDiagnosticsContext.Scope.Dispose
adamnova Oct 31, 2025
7543e13
refactor: Remove position reset from WriteToStream method
adamnova Oct 31, 2025
08c574c
docs: document Activity disposal safety in CosmosDiagnosticsContext.S…
adamnova Oct 31, 2025
d60f257
refactor: move startTicks capture into Scope constructor
adamnova Oct 31, 2025
9010214
docs: document nested spans limitation in CosmosDiagnosticsContext
adamnova Oct 31, 2025
bc13a10
docs: document null owner design pattern in Scope
adamnova Oct 31, 2025
376a99b
docs: document EncryptionDiagnostics design rationale
adamnova Oct 31, 2025
1a4d4b8
refactor: remove obvious comments from CosmosDiagnosticsContext
adamnova Oct 31, 2025
e35778c
docs: Clarify nested scope limitation in CosmosDiagnosticsContext
adamnova Nov 3, 2025
daaa8c4
Move diagnostic scope constants to CosmosDiagnosticsContext and delet…
adamnova Nov 3, 2025
40ad86a
Merge branch 'master' into feature/encryptionprocessor-stream-switch
adamnova Nov 3, 2025
72891dd
Add ArgumentValidation helper methods and update CosmosDiagnosticsCon…
adamnova Nov 4, 2025
2bb59d4
Refactor diagnostics context and add unit tests
adamnova Nov 4, 2025
48dbdd3
Refactor: Replace manual null checks with ArgumentValidation helper +…
adamnova Nov 4, 2025
7f36c6e
Add argument validation for diagnosticsContext parameter and remove n…
adamnova Nov 4, 2025
0b48ae1
Add ArgumentValidation methods for range validation
adamnova Nov 4, 2025
054ca11
Add unit tests for ArgumentValidation utility
adamnova Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,127 @@
namespace Microsoft.Azure.Cosmos.Encryption.Custom
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

/// <summary>
/// This is an empty implementation of CosmosDiagnosticsContext which has been plumbed through the DataEncryptionKeyProvider and EncryptionContainer.
/// This may help adding diagnostics more easily in future.
/// Lightweight diagnostics context for Custom Encryption extension.
/// Records scope names, start/stop timestamps and exposes them for tests or future wiring into SDK diagnostics.
/// Uses <see cref="ActivitySource"/> so downstream telemetry (OpenTelemetry) can optionally subscribe.
/// </summary>
internal class CosmosDiagnosticsContext
{
private static readonly CosmosDiagnosticsContext UnusedSingleton = new ();
private static readonly IDisposable UnusedScopeSingleton = new Scope();
private static readonly ActivitySource ActivitySource = new ("Microsoft.Azure.Cosmos.Encryption.Custom");

private readonly List<ScopeRecord> records = new (4);

internal CosmosDiagnosticsContext()
{
}

/// <summary>
/// Factory. A new instance is created per high-level operation to avoid cross-talk.
/// </summary>
public static CosmosDiagnosticsContext Create(RequestOptions options)
{
_ = options;
return CosmosDiagnosticsContext.UnusedSingleton;
_ = options; // Reserved for future correlation if RequestOptions ever carries a diagnostics handle.
return new CosmosDiagnosticsContext();
}

/// <summary>
/// Recorded scope metadata (immutable snapshot on scope dispose).
/// </summary>
internal readonly struct ScopeRecord
{
public ScopeRecord(string name, long startTimestamp, long elapsedTicks)
{
this.Name = name;
this.StartTimestamp = startTimestamp;
this.ElapsedTicks = elapsedTicks;
}

public string Name { get; }

public long StartTimestamp { get; }

public long ElapsedTicks { get; } // Stopwatch ticks

public TimeSpan Elapsed => TimeSpan.FromTicks(this.ElapsedTicks);
}

/// <summary>
/// Gets recorded scope names primarily for unit tests (copy snapshot each access).
/// </summary>
internal IReadOnlyList<string> Scopes
{
get
{
if (this.records.Count == 0)
{
return Array.Empty<string>();
}

string[] names = new string[this.records.Count];
for (int i = 0; i < this.records.Count; i++)
{
names[i] = this.records[i].Name;
}

return names;
}
}

public Scope CreateScope(string scope)
{
if (string.IsNullOrEmpty(scope))
{
return Scope.Noop; // returns default(struct) => no-op
}

// Only create Activity if there are listeners to avoid unnecessary allocations.
Activity activity = ActivitySource.HasListeners() ? ActivitySource.StartActivity(scope, ActivityKind.Internal) : null;
long startTicks = Stopwatch.GetTimestamp();
return new Scope(this, scope, startTicks, activity);
}

public IDisposable CreateScope(string scope)
private void Record(string name, long startTicks, long elapsedTicks)
{
_ = scope;
return CosmosDiagnosticsContext.UnusedScopeSingleton;
lock (this.records)
{
this.records.Add(new ScopeRecord(name, startTicks, elapsedTicks));
}
}

private class Scope : IDisposable
public readonly struct Scope : IDisposable
{
private readonly CosmosDiagnosticsContext owner;
private readonly string name;
private readonly long startTicks;
private readonly Activity activity;
private readonly bool enabled;

internal Scope(CosmosDiagnosticsContext owner, string name, long startTicks, Activity activity)
{
this.owner = owner;
this.name = name;
this.startTicks = startTicks;
this.activity = activity;
this.enabled = owner != null; // default struct (Noop) => owner null
}

internal static Scope Noop => default;

public void Dispose()
{
if (!this.enabled)
{
return;
}

long elapsedTicks = Stopwatch.GetTimestamp() - this.startTicks;
this.owner.Record(this.name, this.startTicks, elapsedTicks);
this.activity?.Dispose();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,7 @@ internal CosmosJsonDotNetSerializer(JsonSerializerSettings jsonSerializerSetting
/// <returns>The object representing the deserialized stream</returns>
public T FromStream<T>(Stream stream)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(stream);
#else
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
#endif
ArgumentValidation.ThrowIfNull(stream);

if (typeof(Stream).IsAssignableFrom(typeof(T)))
{
Expand Down Expand Up @@ -87,6 +80,44 @@ public MemoryStream ToStream<T>(T input)
return streamPayload;
}

/// <summary>
/// Serializes an object directly into the provided output stream (which remains open) and rewinds it if seekable.
/// </summary>
/// <typeparam name="T">Type of object being serialized.</typeparam>
/// <param name="input">Object to serialize.</param>
/// <param name="output">Destination stream. Must be writable. The stream is not disposed by this method.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="output"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="output"/> is not writable.</exception>
/// <remarks>
/// This method serializes the object directly to the provided stream without creating an intermediate MemoryStream,
/// reducing memory allocations for large objects. If the output stream is seekable, its position is reset to 0 after writing.
/// </remarks>
public void WriteToStream<T>(T input, Stream output)
{
ArgumentValidation.ThrowIfNull(output);

if (!output.CanWrite)
{
throw new ArgumentException("Output stream must be writable", nameof(output));
}

using (StreamWriter streamWriter = new (output, encoding: CosmosJsonDotNetSerializer.DefaultEncoding, bufferSize: 1024, leaveOpen: true))
using (JsonTextWriter writer = new (streamWriter))
{
writer.ArrayPool = JsonArrayPool.Instance;
writer.Formatting = Newtonsoft.Json.Formatting.None;
JsonSerializer jsonSerializer = this.GetSerializer();
jsonSerializer.Serialize(writer, input);
writer.Flush();
streamWriter.Flush();
}

if (output.CanSeek)
{
output.Position = 0;
}
}

/// <summary>
/// JsonSerializer has hit a race conditions with custom settings that cause null reference exception.
/// To avoid the race condition a new JsonSerializer is created for each call
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.Encryption.Custom
{
using System.IO;
using System.Threading.Tasks;

/// <summary>
/// Extension methods for Stream to provide compatibility across different .NET versions.
/// </summary>
internal static class StreamExtensions
{
/// <summary>
/// Asynchronously disposes the stream in a version-compatible way.
/// Uses DisposeAsync on .NET 8.0+ and falls back to synchronous Dispose on earlier versions.
/// </summary>
/// <param name="stream">The stream to dispose.</param>
/// <returns>A ValueTask representing the asynchronous dispose operation.</returns>
public static ValueTask DisposeCompatAsync(this Stream stream)
{
#if NET8_0_OR_GREATER
return stream.DisposeAsync();
#else
stream.Dispose();
return default;
#endif
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,7 @@ public async Task InitializeAsync(
throw new InvalidOperationException($"{nameof(CosmosDataEncryptionKeyProvider)} has already been initialized.");
}

#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(database);
#else
if (database == null)
{
throw new ArgumentNullException(nameof(database));
}
#endif
ArgumentValidation.ThrowIfNull(database);

ContainerResponse containerResponse = await database.CreateContainerIfNotExistsAsync(
containerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,7 @@ public static DataEncryptionKey Create(
byte[] rawKey,
string encryptionAlgorithm)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(rawKey);
#else
if (rawKey == null)
{
throw new ArgumentNullException(nameof(rawKey));
}
#endif
ArgumentValidation.ThrowIfNull(rawKey);

#pragma warning disable CS0618 // Type or member is obsolete
if (!string.Equals(encryptionAlgorithm, CosmosEncryptionAlgorithm.AEAes256CbcHmacSha256Randomized))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,7 @@ public override async Task<ItemResponse<DataEncryptionKeyProperties>> CreateData
throw new ArgumentException(string.Format("Unsupported Encryption Algorithm {0}", encryptionAlgorithm), nameof(encryptionAlgorithm));
}

#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(encryptionKeyWrapMetadata);
#else
if (encryptionKeyWrapMetadata == null)
{
throw new ArgumentNullException(nameof(encryptionKeyWrapMetadata));
}
#endif
ArgumentValidation.ThrowIfNull(encryptionKeyWrapMetadata);

CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions);

Expand Down Expand Up @@ -159,14 +152,7 @@ public override async Task<ItemResponse<DataEncryptionKeyProperties>> RewrapData
ItemRequestOptions requestOptions = null,
CancellationToken cancellationToken = default)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(newWrapMetadata);
#else
if (newWrapMetadata == null)
{
throw new ArgumentNullException(nameof(newWrapMetadata));
}
#endif
ArgumentValidation.ThrowIfNull(newWrapMetadata);

CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,7 @@ public override Task<ItemResponse<DataEncryptionKeyProperties>> RewrapDataEncryp
throw new ArgumentNullException(nameof(id));
}

#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(newWrapMetadata);
#else
if (newWrapMetadata == null)
{
throw new ArgumentNullException(nameof(newWrapMetadata));
}
#endif
ArgumentValidation.ThrowIfNull(newWrapMetadata);

return TaskHelper.RunInlineIfNeededAsync(() =>
this.dataEncryptionKeyContainerCore.RewrapDataEncryptionKeyAsync(id, newWrapMetadata, encryptionAlgorithm, requestOptions, cancellationToken));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,7 @@ internal static DecryptableFeedResponse<T> CreateResponse(
ResponseMessage responseMessage,
IReadOnlyCollection<T> resource)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(responseMessage);
#else
if (responseMessage == null)
{
throw new ArgumentNullException(nameof(responseMessage));
}
#endif
ArgumentValidation.ThrowIfNull(responseMessage);

using (responseMessage)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
namespace Microsoft.Azure.Cosmos.Encryption.Custom
{
internal static class EncryptionDiagnostics
{
internal const string ScopeEncryptModeSelectionPrefix = "EncryptionProcessor.Encrypt.Mde.";
internal const string ScopeDecryptModeSelectionPrefix = "EncryptionProcessor.Decrypt.Mde.";
}
}
Loading