Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core.Experimental] Add DynamicContent/DynamicRequest/DynamicResponse for LLC prototype #18323

Merged
merged 9 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
@@ -1,5 +1,14 @@
namespace Azure.Core
{
[System.Diagnostics.DebuggerDisplayAttribute("Content: {_body}")]
public partial class DynamicContent : Azure.Core.RequestContent
{
internal DynamicContent() { }
public override void Dispose() { }
public override bool TryComputeLength(out long length) { throw null; }
public override void WriteTo(System.IO.Stream stream, System.Threading.CancellationToken cancellation) { }
public override System.Threading.Tasks.Task WriteToAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellation) { throw null; }
}
public partial class DynamicJson : System.Dynamic.IDynamicMetaObjectProvider
{
public DynamicJson(string json) { }
Expand Down Expand Up @@ -56,6 +65,31 @@ public DynamicJson(System.Text.Json.JsonElement element) { }
public override string ToString() { throw null; }
public void WriteTo(System.Text.Json.Utf8JsonWriter writer) { }
}
[System.Diagnostics.DebuggerDisplayAttribute("Body: {Body}")]
public partial class DynamicRequest : Azure.Core.Request
{
public DynamicRequest(Azure.Core.Request request, Azure.Core.Pipeline.HttpPipeline pipeline) { }
public Azure.Core.DynamicJson Body { get { throw null; } set { } }
public override string ClientRequestId { get { throw null; } set { } }
public override Azure.Core.RequestContent? Content { get { throw null; } set { } }
protected override void AddHeader(string name, string value) { }
protected override bool ContainsHeader(string name) { throw null; }
public override void Dispose() { }
protected virtual void Dispose(bool disposing) { }
protected override System.Collections.Generic.IEnumerable<Azure.Core.HttpHeader> EnumerateHeaders() { throw null; }
protected override bool RemoveHeader(string name) { throw null; }
public Azure.Core.DynamicResponse Send(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Threading.Tasks.Task<Azure.Core.DynamicResponse> SendAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
protected override bool TryGetHeader(string name, out string? value) { throw null; }
protected override bool TryGetHeaderValues(string name, out System.Collections.Generic.IEnumerable<string>? values) { throw null; }
}
[System.Diagnostics.DebuggerDisplayAttribute("Status: {Response.Status}, Value: {Value}")]
public partial class DynamicResponse : Azure.Response<Azure.Core.DynamicJson>
{
public DynamicResponse(Azure.Response response, Azure.Core.DynamicJson value) { }
public override Azure.Core.DynamicJson Value { get { throw null; } }
public override Azure.Response GetRawResponse() { throw null; }
}
}
namespace Azure.Core.GeoJson
{
Expand Down
60 changes: 60 additions & 0 deletions sdk/core/Azure.Core.Experimental/src/DynamicContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace Azure.Core
{
/// <summary>
/// Represents the <see cref="DynamicJson"/> sent as part of the Azure.Core.Request.
/// </summary>
[DebuggerDisplay("Content: {_body}")]
public class DynamicContent : RequestContent
{
private readonly DynamicJson _body;

internal DynamicContent(DynamicJson body)
{
_body = body;
}

internal static RequestContent Create(DynamicJson body) => new DynamicContent(body);

/// <inheritdoc />
public override async Task WriteToAsync(Stream stream, CancellationToken cancellation)
{
using Utf8JsonWriter writer = new Utf8JsonWriter(stream);
_body.WriteTo(writer);
await writer.FlushAsync(cancellation).ConfigureAwait(false);
}

/// <inheritdoc />
public override void WriteTo(Stream stream, CancellationToken cancellation)
{
using Utf8JsonWriter writer = new Utf8JsonWriter(stream);
_body.WriteTo(writer);
writer.Flush();
}

/// <inheritdoc />
public override bool TryComputeLength(out long length)
{
using MemoryStream stream = new MemoryStream();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very expensive way to calculate the length. We can at least try to cache to resulting content.

Copy link
Contributor

Choose a reason for hiding this comment

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

It might be hard because the referenced DynamicJson can change.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should do the following:

  1. DynamicJson needs to be an union of the current hashtable and Memory.
  2. DynamicJson need to know when the hashtable and the memory are our of sync.
  3. DynamicJson needs to have a method to explicitly write the hashtable to memory, if they are our of sync.
  4. Changes to hashtable invalidate the memory.
  5. The TryGetLength method would return true if memory and hashtable are in sync.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this is my unfortunate doing. While working on a prototype I found that without implementing this we would get chunked encoding for the request and that was causing a problem with a service I was testing. It is hard with DynamicJson being mutable.

How often is TryComputeLength called on average per request? I would have expected just once if it returns true?

Copy link
Contributor

Choose a reason for hiding this comment

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

How often is TryComputeLength called on average per request?

once

WriteTo(stream, CancellationToken.None);
length = Encoding.UTF8.GetString(stream.ToArray()).Length;
return true;
}

/// <inheritdoc />
public override void Dispose()
Copy link
Member

Choose a reason for hiding this comment

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

I think we should properly implement the Dispose pattern here, i.e. Dispose(bool), or seal the type.
Also, I don't think you need to suppress finalization, given this type is not finalizable, nor current FDGs recommend creating public finalizable types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Even sealed I get:

Error	CS0534	'DynamicContent' does not implement inherited abstract member 'RequestContent.Dispose()'	

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or did you mean seal it and then just GC.SuppressFinalize is fine?

Copy link
Member

Choose a reason for hiding this comment

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

What I meant is that if you seal, you don't need to implement full dispose pattern (Dispose(bool). If you don't seal, you need to.

{
GC.SuppressFinalize(this);
}
}
}
167 changes: 167 additions & 0 deletions sdk/core/Azure.Core.Experimental/src/DynamicRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Core;
using Azure.Core.Pipeline;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace Azure.Core
{
/// <summary>
/// Represents an HTTP request with <see cref="DynamicJson"/> content.
/// </summary>
[DebuggerDisplay("Body: {Body}")]
public class DynamicRequest : Request
{
private Request Request { get; }
private HttpPipeline HttpPipeline { get; }
private bool _disposed;

private static readonly Encoding Utf8NoBom = new UTF8Encoding(false, true);

/// <inheritdoc />
public override RequestContent? Content
{
get => DynamicContent.Create(Body);

set {
MemoryStream ms = new MemoryStream();
if (value != null)
{
value.WriteTo(ms, default);
ms.Seek(0, SeekOrigin.Begin);
}
using (StreamReader sr = new StreamReader(ms, Utf8NoBom))
{
Body = new DynamicJson(sr.ReadToEnd());
}
Request.Content = value;
}
}

// TODO(matell): How does the initialization here play into the ability to send a request with an "empty" body?
/// <summary>
/// The JSON body of request.
/// </summary>
public DynamicJson Body { get; set; } = DynamicJson.Object();

// TODO(matell): In Krzysztof's prototype we also took DiagnosticScope as a parameter, do we still need that?
/// <summary>
/// Creates an instance of <see cref="RequestContent"/> that wraps <see cref="DynamicJson"/> content.
/// </summary>
/// <param name="request">The <see cref="Request"/> to send.</param>
/// <param name="pipeline">The HTTP pipeline for sending and receiving REST requests and responses.</param>
public DynamicRequest(Request request, HttpPipeline pipeline)
{
Request = request;
HttpPipeline = pipeline;
}

/// <summary>
/// Send the request asynchronously.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The response dynamically typed in a <see cref="DynamicResponse"/>.</returns>
public async Task<DynamicResponse> SendAsync(CancellationToken cancellationToken = default)
{
// Since we are sending the underlying request, we need to copy the Content on to it, or we'll lose the body.
Request.Content = Content;

Response res = await HttpPipeline.SendRequestAsync(Request, cancellationToken).ConfigureAwait(false);
DynamicJson dynamicContent;

if (res.ContentStream != null)
{
JsonDocument doc = await JsonDocument.ParseAsync(res.ContentStream, new JsonDocumentOptions(), cancellationToken).ConfigureAwait(false);
dynamicContent = new DynamicJson(doc.RootElement);
}
else
{
dynamicContent = new DynamicJson(JsonDocument.Parse("null").RootElement);
}

return new DynamicResponse(res, dynamicContent);
}

/// <summary>
/// Send the request synchronously.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The response dynamically typed in a <see cref="DynamicResponse"/>.</returns>
public DynamicResponse Send(CancellationToken cancellationToken = default)
{
// Since we are sending the underlying request, we need to copy the Content on to it, or we'll lose the body.
Request.Content = Content;

Response res = HttpPipeline.SendRequest(Request, cancellationToken);
DynamicJson dynamicContent;

if (res.ContentStream != null)
{
JsonDocument doc = JsonDocument.Parse(res.ContentStream);
dynamicContent = new DynamicJson(doc.RootElement);
}
else
{
dynamicContent = new DynamicJson(JsonDocument.Parse("null").RootElement);
}

return new DynamicResponse(res, dynamicContent);
}

/// <inheritdoc />
public override string ClientRequestId { get => Request.ClientRequestId; set => Request.ClientRequestId = value; }

/// <summary>
/// Frees resources held by the <see cref="DynamicRequest"/> object.
/// </summary>
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Frees resources held by the <see cref="DynamicRequest"/> object.
/// </summary>
/// <param name="disposing">true if we should dispose, otherwise false</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
Request.Dispose();
}
_disposed = true;
}

/// <inheritdoc />
protected override void AddHeader(string name, string value) => Request.Headers.Add(name, value);

/// <inheritdoc />
protected override bool ContainsHeader(string name) => Request.Headers.Contains(name);

/// <inheritdoc />
protected override IEnumerable<HttpHeader> EnumerateHeaders() => Request.Headers;

/// <inheritdoc />
protected override bool RemoveHeader(string name) => Request.Headers.Remove(name);

/// <inheritdoc />
protected override bool TryGetHeader(string name, [NotNullWhen(true)] out string? value) => Request.Headers.TryGetValue(name, out value);

/// <inheritdoc />
protected override bool TryGetHeaderValues(string name, [NotNullWhen(true)] out IEnumerable<string>? values) => Request.Headers.TryGetValues(name, out values);
}
}
36 changes: 36 additions & 0 deletions sdk/core/Azure.Core.Experimental/src/DynamicResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace Azure.Core
{
/// <summary>
/// Represents a result of Azure operation with a <see cref="DynamicJson"/> response.
/// </summary>
[DebuggerDisplay("Status: {Response.Status}, Value: {Value}")]
public class DynamicResponse : Response<DynamicJson>
{
private Response Response { get; }

/// <inheritdoc />>
public override DynamicJson Value { get; }

/// <inheritdoc />
public override Response GetRawResponse() => Response;

/// <summary>
/// Represents a result of Azure operation with a <see cref="DynamicJson"/> response.
/// </summary>
/// <param name="response">The response returned by the service.</param>
/// <param name="value">The value returned by the service.</param>
public DynamicResponse(Response response, DynamicJson value)
{
Response = response;
Value = value;
}
}
}