Skip to content

Commit

Permalink
gRPC-Web - Add .NET Standard 2.0 support (#1203)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Feb 17, 2021
1 parent 3c675be commit 08024e3
Show file tree
Hide file tree
Showing 35 changed files with 470 additions and 109 deletions.
1 change: 1 addition & 0 deletions build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<SystemCommandLineRenderingPackageVersion>0.3.0-alpha.20214.1</SystemCommandLineRenderingPackageVersion>
<SystemDiagnosticsDiagnosticSourcePackageVersion>4.5.1</SystemDiagnosticsDiagnosticSourcePackageVersion>
<SystemIOPipelinesPackageVersion>4.7.2</SystemIOPipelinesPackageVersion>
<SystemMemoryPackageVersion>4.5.3</SystemMemoryPackageVersion>
<SystemNetHttpWinHttpHandlerPackageVersion>5.0.0</SystemNetHttpWinHttpHandlerPackageVersion>
<SystemSecurityPrincipalWindowsPackageVersion>4.6.0</SystemSecurityPrincipalWindowsPackageVersion>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Compile Include="..\..\test\Shared\TestHttpMessageHandler.cs" Link="Internal\TestHttpMessageHandler.cs" />
<Compile Include="..\..\test\Shared\TestRequestBodyPipeFeature.cs" Link="Internal\TestRequestBodyPipeFeature.cs" />
<Compile Include="..\..\test\Shared\TestResponseBodyFeature.cs" Link="Internal\TestResponseBodyFeature.cs" />
<Compile Include="..\..\src\Shared\TrailingHeadersHelpers.cs" Link="Internal\TrailingHeadersHelpers.cs" />

<FrameworkReference Include="Microsoft.AspNetCore.App" />

Expand Down
7 changes: 6 additions & 1 deletion src/Grpc.Net.Client.Web/Grpc.Net.Client.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@

<IsGrpcPublishedPackage>true</IsGrpcPublishedPackage>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TargetFrameworks>netstandard2.1;net5.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net5.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Shared\CommonGrpcProtocolHelpers.cs" Link="Internal\CommonGrpcProtocolHelpers.cs" />
<Compile Include="..\Shared\TrailingHeadersHelpers.cs" Link="Internal\TrailingHeadersHelpers.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="System.Memory" Version="$(SystemMemoryPackageVersion)" />
</ItemGroup>

</Project>
9 changes: 7 additions & 2 deletions src/Grpc.Net.Client.Web/GrpcWebHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ private async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request

if (response.Content != null && IsMatchingResponseContentType(GrpcWebMode, response.Content.Headers.ContentType?.MediaType))
{
response.Content = new GrpcWebResponseContent(response.Content, GrpcWebMode, response.TrailingHeaders);
#if NETSTANDARD2_0
// In netstandard2.0 we look for headers in request properties. Need to create them.
response.EnsureTrailingHeaders();
#endif

response.Content = new GrpcWebResponseContent(response.Content, GrpcWebMode, response.TrailingHeaders());
}

// The gRPC client validates HTTP version 2.0 and will error if it isn't. Always set
Expand All @@ -167,7 +172,7 @@ private async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request
//
// Note: Some handlers don't correctly set HttpResponseMessage.Version.
// We can't rely on it being set correctly. It is safest to always set it to 2.0.
response.Version = System.Net.HttpVersion.Version20;
response.Version = GrpcWebProtocolConstants.Http2Version;

return response;
}
Expand Down
24 changes: 18 additions & 6 deletions src/Grpc.Net.Client.Web/Internal/Base64RequestStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ public Base64RequestStream(Stream inner)
_inner = inner;
}

#if NETSTANDARD2_0
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
#else
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
#endif
{
#if NETSTANDARD2_0
var data = buffer.AsMemory(offset, count);
#endif

if (_buffer == null)
{
_buffer = ArrayPool<byte>.Shared.Rent(minimumLength: 4096);
Expand Down Expand Up @@ -84,7 +92,7 @@ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> data, Cancellati

EnsureSuccess(
Base64.EncodeToUtf8(data.Span.Slice(0, encodeLength), localBuffer.Span, out var bytesConsumed, out var bytesWritten, isFinalBlock: false),
#if NETSTANDARD2_1
#if NETSTANDARD2_1 || NETSTANDARD2_0
OperationStatus.NeedMoreData
#else
// React to fix https://github.com/dotnet/runtime/pull/281
Expand All @@ -93,7 +101,7 @@ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> data, Cancellati
);

var base64Remainder = _buffer.Length - localBuffer.Length;
await _inner.WriteAsync(_buffer.AsMemory(0, bytesWritten + base64Remainder), cancellationToken).ConfigureAwait(false);
await StreamHelpers.WriteAsync(_inner, _buffer, 0, bytesWritten + base64Remainder, cancellationToken).ConfigureAwait(false);

data = data.Slice(bytesConsumed);
localBuffer = _buffer;
Expand All @@ -103,7 +111,7 @@ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> data, Cancellati
// If there was not enough data to write along with remainder then write it here
if (localBuffer.Length < _buffer.Length)
{
await _inner.WriteAsync(_buffer.AsMemory(0, 4), cancellationToken).ConfigureAwait(false);
await StreamHelpers.WriteAsync(_inner, _buffer, 0, 4, cancellationToken).ConfigureAwait(false);
}

if (data.Length > 0)
Expand Down Expand Up @@ -135,7 +143,7 @@ internal async Task WriteRemainderAsync(CancellationToken cancellationToken)
{
EnsureSuccess(Base64.EncodeToUtf8InPlace(_buffer, _remainder, out var bytesWritten));

await _inner.WriteAsync(_buffer.AsMemory(0, bytesWritten), cancellationToken).ConfigureAwait(false);
await StreamHelpers.WriteAsync(_inner, _buffer!, 0, bytesWritten, cancellationToken).ConfigureAwait(false);
_remainder = 0;
}
}
Expand All @@ -152,7 +160,7 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}

#region Stream implementation
#region Stream implementation
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => _inner.CanWrite;
Expand Down Expand Up @@ -186,9 +194,13 @@ public override void SetLength(long value)
public override void Write(byte[] buffer, int offset, int count)
{
// Used by unit tests
#if NETSTANDARD2_0
WriteAsync(buffer, 0, count).GetAwaiter().GetResult();
#else
WriteAsync(buffer.AsMemory(0, count)).AsTask().GetAwaiter().GetResult();
#endif
FlushAsync().GetAwaiter().GetResult();
}
#endregion
#endregion
}
}
11 changes: 10 additions & 1 deletion src/Grpc.Net.Client.Web/Internal/Base64ResponseStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.Buffers.Text;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -40,8 +41,16 @@ public Base64ResponseStream(Stream inner)
_inner = inner;
}

#if NETSTANDARD2_0
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
#else
public override async ValueTask<int> ReadAsync(Memory<byte> data, CancellationToken cancellationToken = default)
#endif
{
#if NETSTANDARD2_0
var data = buffer.AsMemory(offset, count);
#endif

// There is enough remaining data to fill passed in data
if (data.Length <= _remainder)
{
Expand Down Expand Up @@ -73,7 +82,7 @@ public override async ValueTask<int> ReadAsync(Memory<byte> data, CancellationTo
// Minimum valid base64 length is 4. Read until we have at least that much content
do
{
var read = await _inner.ReadAsync(availableReadData, cancellationToken).ConfigureAwait(false);
var read = await StreamHelpers.ReadAsync(_inner, availableReadData, cancellationToken).ConfigureAwait(false);
if (read == 0)
{
if (_remainder > 0)
Expand Down
7 changes: 7 additions & 0 deletions src/Grpc.Net.Client.Web/Internal/GrpcWebProtocolConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@

#endregion

using System;
using System.Net.Http.Headers;

namespace Grpc.Net.Client.Web.Internal
{
internal static class GrpcWebProtocolConstants
{
#if !NETSTANDARD2_0
public static readonly Version Http2Version = System.Net.HttpVersion.Version20;
#else
public static readonly Version Http2Version = new Version(2, 0);
#endif

public const string GrpcContentType = "application/grpc";
public const string GrpcWebContentType = "application/grpc-web";
public const string GrpcWebTextContentType = "application/grpc-web-text";
Expand Down
14 changes: 14 additions & 0 deletions src/Grpc.Net.Client.Web/Internal/GrpcWebRequestContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ private async Task SerializeTextToStreamAsync(Stream stream)

protected override bool TryComputeLength(out long length)
{
// ContentLength is calculated using the inner content's TryComputeLength.
var contentLength = _inner.Headers.ContentLength;
if (contentLength != null)
{
// When a grpc-web-text call is sent the request is base64 encoded.
// If a content-length is specified then we need to change the value
// to take into account base64 encoding size difference:
// Increase length by 4/3, then round up to the next multiple of 4.
length = _mode == GrpcWebMode.GrpcWebText
? ((4 * contentLength.GetValueOrDefault() / 3) + 3) & ~3
: contentLength.GetValueOrDefault();
return true;
}

length = -1;
return false;
}
Expand Down
27 changes: 22 additions & 5 deletions src/Grpc.Net.Client.Web/Internal/GrpcWebResponseStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,16 @@ public GrpcWebResponseStream(Stream inner, HttpHeaders responseTrailers)
_responseTrailers = responseTrailers;
}

#if NETSTANDARD2_0
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
#else
public override async ValueTask<int> ReadAsync(Memory<byte> data, CancellationToken cancellationToken = default)
#endif
{
#if NETSTANDARD2_0
var data = buffer.AsMemory(offset, count);
#endif

switch (_state)
{
case ResponseState.Ready:
Expand Down Expand Up @@ -95,7 +103,7 @@ public override async ValueTask<int> ReadAsync(Memory<byte> data, CancellationTo
data = data.Slice(0, _contentRemaining);
}

var read = await _inner.ReadAsync(data, cancellationToken).ConfigureAwait(false);
var read = await StreamHelpers.ReadAsync(_inner, data, cancellationToken).ConfigureAwait(false);
_contentRemaining -= read;
if (_contentRemaining == 0)
{
Expand Down Expand Up @@ -137,7 +145,7 @@ private async Task ReadTrailersAsync(int trailerLength, Memory<byte> data, Cance
// 2. The response stream is read to completion. HttpClient may not recognize the
// request as completing successfully if the request and response aren't completely
// consumed.
var count = await _inner.ReadAsync(data, cancellationToken).ConfigureAwait(false);
var count = await StreamHelpers.ReadAsync(_inner, data, cancellationToken).ConfigureAwait(false);
if (count > 0)
{
throw new InvalidOperationException("Unexpected data after trailers.");
Expand Down Expand Up @@ -181,14 +189,23 @@ private void ParseTrailers(ReadOnlySpan<byte> span)
throw new InvalidOperationException("Error parsing badly formatted trailing header.");
}

var name = Encoding.ASCII.GetString(Trim(line.Slice(0, headerDelimiterIndex)));
var value = Encoding.ASCII.GetString(Trim(line.Slice(headerDelimiterIndex + 1)));
var name = GetString(Trim(line.Slice(0, headerDelimiterIndex)));
var value = GetString(Trim(line.Slice(headerDelimiterIndex + 1)));

_responseTrailers.Add(name, value);
}
}
}

private static string GetString(ReadOnlySpan<byte> span)
{
#if NETSTANDARD2_0
return Encoding.ASCII.GetString(span.ToArray());
#else
return Encoding.ASCII.GetString(span);
#endif
}

internal static ReadOnlySpan<byte> Trim(ReadOnlySpan<byte> span)
{
var startIndex = -1;
Expand Down Expand Up @@ -228,7 +245,7 @@ private static async Task<bool> TryReadDataAsync(Stream responseStream, Memory<b
{
int read;
var received = 0;
while ((read = await responseStream.ReadAsync(buffer.Slice(received, buffer.Length - received), cancellationToken).ConfigureAwait(false)) > 0)
while ((read = await StreamHelpers.ReadAsync(responseStream, buffer.Slice(received, buffer.Length - received), cancellationToken).ConfigureAwait(false)) > 0)
{
received += read;

Expand Down
63 changes: 63 additions & 0 deletions src/Grpc.Net.Client.Web/Internal/StreamHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Grpc.Net.Client.Web.Internal
{
internal static class StreamHelpers
{
/// <summary>
/// WriteAsync uses the best overload for the platform.
/// </summary>
#if NETSTANDARD2_0
public static Task WriteAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return stream.WriteAsync(buffer, offset, count, cancellationToken);
}
#else
public static ValueTask WriteAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return stream.WriteAsync(buffer.AsMemory(offset, count), cancellationToken);
}
#endif

/// <summary>
/// ReadAsync uses the best overload for the platform. The data must be backed by an array.
/// </summary>
#if NETSTANDARD2_0
public static Task<int> ReadAsync(Stream stream, Memory<byte> data, CancellationToken cancellationToken = default)
{
var success = MemoryMarshal.TryGetArray<byte>(data, out var segment);
Debug.Assert(success);
return stream.ReadAsync(segment.Array, segment.Offset, segment.Count, cancellationToken);
}
#else
public static ValueTask<int> ReadAsync(Stream stream, Memory<byte> data, CancellationToken cancellationToken = default)
{
return stream.ReadAsync(data, cancellationToken);
}
#endif
}
}
1 change: 1 addition & 0 deletions src/Grpc.Net.Client/Grpc.Net.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Compile Include="..\Shared\DefaultDeserializationContext.cs" Link="Internal\DefaultDeserializationContext.cs" />
<Compile Include="..\Shared\HttpHandlerFactory.cs" Link="Internal\Http\HttpHandlerFactory.cs" />
<Compile Include="..\Shared\TelemetryHeaderHandler.cs" Link="Internal\Http\TelemetryHeaderHandler.cs" />
<Compile Include="..\Shared\TrailingHeadersHelpers.cs" Link="Internal\Http\TrailingHeadersHelpers.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
20 changes: 0 additions & 20 deletions src/Grpc.Net.Client/Internal/CompatibilityExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,6 @@ namespace Grpc.Net.Client.Internal
{
internal static class CompatibilityExtensions
{
#if !NETSTANDARD2_0
public static readonly Version Version20 = HttpVersion.Version20;
#else
public static readonly Version Version20 = new Version(2, 0);
public static readonly string ResponseTrailersKey = "__ResponseTrailers";
#endif

public static HttpHeaders GetTrailingHeaders(this HttpResponseMessage responseMessage)
{
#if !NETSTANDARD2_0
return responseMessage.TrailingHeaders;
#else
if (!responseMessage.RequestMessage.Properties.TryGetValue(ResponseTrailersKey, out var headers))
{
throw new InvalidOperationException();
}
return (HttpHeaders)headers;
#endif
}

[Conditional("DEBUG")]
public static void Assert([DoesNotReturnIf(false)] bool condition, string? message = null)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Grpc.Net.Client/Internal/GrpcCall.NonGeneric.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ protected bool TryGetTrailers([NotNullWhen(true)] out Metadata? trailers)
}

CompatibilityExtensions.Assert(HttpResponse != null);
Trailers = GrpcProtocolHelpers.BuildMetadata(HttpResponse.GetTrailingHeaders());
Trailers = GrpcProtocolHelpers.BuildMetadata(HttpResponse.TrailingHeaders());
}

trailers = Trailers;
Expand Down
Loading

0 comments on commit 08024e3

Please sign in to comment.