Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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 @@ -95,8 +95,8 @@ private static void EnrichRequestMessage(RequestMessage requestMessage, Distribu
{
// Set DTC-specific headers
requestMessage.Headers.Add(HttpConstants.HttpHeaders.IdempotencyToken, serverRequest.IdempotencyToken.ToString());
requestMessage.Headers.Add(HttpConstants.HttpHeaders.OperationType, requestMessage.OperationType.ToString());
requestMessage.Headers.Add(HttpConstants.HttpHeaders.ResourceType, requestMessage.ResourceType.ToString());
requestMessage.Headers.Add(HttpConstants.HttpHeaders.OperationType, requestMessage.OperationType.ToOperationTypeString());
requestMessage.Headers.Add(HttpConstants.HttpHeaders.ResourceType, requestMessage.ResourceType.ToResourceTypeString());
requestMessage.UseGatewayMode = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ internal DistributedTransactionOperationResult(DistributedTransactionOperationRe

/// <summary>
/// Initializes a new instance of the <see cref="DistributedTransactionOperationResult"/> class.
/// This protected constructor is intended for use by derived classes.
/// </summary>
/// <remarks>
/// Must be <c>public</c> for System.Text.Json reflection-based deserialization.
/// System.Text.Json 6.x only scans <c>BindingFlags.Public</c> constructors when resolving
/// <see cref="JsonConstructorAttribute"/>; non-public constructors are not found.
/// Support for non-public constructors was added in System.Text.Json 7.0.
/// </remarks>
[JsonConstructor]
protected DistributedTransactionOperationResult()
public DistributedTransactionOperationResult()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Recommendation · API Surface: Constructor Visibility

[JsonConstructor] changed from protected to public

While the class is normally internal (via #if INTERNAL), when compiled as public, this lets consumers construct result objects with default (zero) values, bypassing SDK construction logic. Result types are typically not meant to be user-constructed.

If [JsonConstructor] requires accessibility for deserialization, consider whether internal would suffice (it does for System.Text.Json when the type itself is internal).


⚠️ AI-generated review — may be incorrect. Agree? → resolve the conversation. Disagree? → reply with your reasoning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The project resolves System.Text.Json 6.0.10. In STJ 6.x, [JsonConstructor] is only honored on public constructors. internal would silently break deserialization.

{
}

Expand All @@ -60,7 +65,7 @@ protected DistributedTransactionOperationResult()
/// Gets the HTTP status code returned by the operation.
/// </summary>
[JsonInclude]
[JsonPropertyName("statuscode")]
[JsonPropertyName("statusCode")]
Comment thread
Meghana-Palaparthi marked this conversation as resolved.
public virtual HttpStatusCode StatusCode { get; internal set; }

/// <summary>
Expand Down Expand Up @@ -91,33 +96,27 @@ protected DistributedTransactionOperationResult()
[JsonIgnore]
public virtual Stream ResourceStream { get; internal set; }

/// <summary>
/// Used for JSON deserialization of the base64-encoded resource body.
/// </summary>
[JsonInclude]
[JsonPropertyName("resourcebody")]
internal string ResourceBodyBase64
{
get => null; // Write-only for deserialization
set
{
if (!string.IsNullOrEmpty(value))
{
byte[] resourceBody = Convert.FromBase64String(value);
this.ResourceStream = new MemoryStream(resourceBody, 0, resourceBody.Length, writable: false, publiclyVisible: true);
}
}
}

/// <summary>
/// Request charge in request units for the operation.
/// </summary>
[JsonInclude]
[JsonPropertyName("requestCharge")]
internal virtual double RequestCharge { get; set; }
public virtual double RequestCharge { get; internal set; }
Comment thread
Meghana-Palaparthi marked this conversation as resolved.

[JsonPropertyName("substatuscode")]
[JsonIgnore]
internal virtual SubStatusCodes SubStatusCode { get; set; }

/// <summary>
/// Gets the sub-status code value as an unsigned integer.
/// </summary>
[JsonInclude]
[JsonPropertyName("subStatusCode")]
public virtual uint SubStatusCodeValue
Comment thread
Meghana-Palaparthi marked this conversation as resolved.
{
get => (uint)this.SubStatusCode;
internal set => this.SubStatusCode = (SubStatusCodes)value;
}

/// <summary>
/// ActivityId related to the operation.
/// </summary>
Expand All @@ -127,14 +126,35 @@ internal string ResourceBodyBase64
[JsonIgnore]
internal ITrace Trace { get; set; }

private static readonly JsonSerializerOptions CaseInsensitiveOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};

/// <summary>
/// Creates a <see cref="DistributedTransactionOperationResult"/> from a JSON element.
/// </summary>
/// <param name="json">The JSON element containing the operation result.</param>
/// <returns>The deserialized operation result.</returns>
internal static DistributedTransactionOperationResult FromJson(JsonElement json)
{
return JsonSerializer.Deserialize<DistributedTransactionOperationResult>(json);
DistributedTransactionOperationResult result = JsonSerializer.Deserialize<DistributedTransactionOperationResult>(json, DistributedTransactionOperationResult.CaseInsensitiveOptions);

if (json.TryGetProperty("resourceBody", out JsonElement resourceBody)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Suggestion · Maintainability: Mixed Deserialization Pattern

FromJson mixes automatic and manual deserialization

This method uses JsonSerializer.Deserialize for most properties, then manually processes resourceBody via TryGetProperty. This dual pattern is fragile — any future property needing special handling requires updates in both the class attributes AND the manual code in FromJson, with no compile-time safety net.

Consider a custom JsonConverter<DistributedTransactionOperationResult> to centralize all deserialization logic in one place.


⚠️ AI-generated review — may be incorrect. Agree? → resolve the conversation. Disagree? → reply with your reasoning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

resourcebody is the only property that requires custom handling. Changing deserialization of other properties to reflect the deserialization of resourcebody could introduce a lot of unnecessary manual handling

&& resourceBody.ValueKind != JsonValueKind.Undefined
&& resourceBody.ValueKind != JsonValueKind.Null)
{
// resourceBody is expected to be a JSON object (Cosmos DB document)
if (resourceBody.ValueKind != JsonValueKind.Object)
Comment thread
Meghana-Palaparthi marked this conversation as resolved.
{
throw new JsonException($"The 'resourceBody' value must be a JSON object, but was '{resourceBody.ValueKind}'.");
}

byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(resourceBody);
result.ResourceStream = new MemoryStream(bytes, 0, bytes.Length, writable: false, publiclyVisible: true);
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,6 @@ private static async Task<DistributedTransactionResponse> PopulateFromJsonConten

DistributedTransactionOperationResult operationResult = DistributedTransactionOperationResult.FromJson(operationElement);
operationResult.Trace = trace;
operationResult.SessionToken ??= responseMessage.Headers.Session;
Comment thread
Meghana-Palaparthi marked this conversation as resolved.
operationResult.ActivityId = responseMessage.Headers.ActivityId;
results.Add(operationResult);
}
Expand Down Expand Up @@ -370,7 +369,6 @@ private void CreateAndPopulateResults(
this.results.Add(new DistributedTransactionOperationResult(this.StatusCode)
{
SubStatusCode = this.SubStatusCode,
SessionToken = this.Headers?.Session,
ActivityId = this.ActivityId,
Trace = trace
});
Expand Down
Loading
Loading