Skip to content

Commit

Permalink
Merge pull request #894 from Cysharp/feature/RefactorUnaryResult
Browse files Browse the repository at this point in the history
Refactor UnaryResult
  • Loading branch information
mayuki authored Jan 8, 2025
2 parents aedf3e5 + a7ea176 commit 46d96ac
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ public UnaryResult Task
{
if (hasResult)
{
return new UnaryResult(Nil.Default);
return default;
}

useBuilder = true;
return new UnaryResult(methodBuilder.Task);
return new UnaryResult((Task)methodBuilder.Task);
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/MagicOnion.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ MagicOnion.ServerStreamingResult<TResponse>.ResponseStream.get -> Grpc.Core.IAsy
MagicOnion.ServerStreamingResult<TResponse>.ServerStreamingResult() -> void
MagicOnion.ServerStreamingResult<TResponse>.ServerStreamingResult(MagicOnion.Internal.IAsyncServerStreamingCallWrapper<TResponse>! inner) -> void
MagicOnion.UnaryResult
MagicOnion.UnaryResult.ConfigureAwait(bool continueOnCapturedContext) -> System.Runtime.CompilerServices.ConfiguredTaskAwaitable
MagicOnion.UnaryResult.Dispose() -> void
MagicOnion.UnaryResult.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter
MagicOnion.UnaryResult.GetStatus() -> Grpc.Core.Status
Expand All @@ -142,9 +143,11 @@ MagicOnion.UnaryResult.ResponseAsync.get -> System.Threading.Tasks.Task!
MagicOnion.UnaryResult.ResponseHeadersAsync.get -> System.Threading.Tasks.Task<Grpc.Core.Metadata!>!
MagicOnion.UnaryResult.UnaryResult() -> void
MagicOnion.UnaryResult.UnaryResult(MessagePack.Nil nil) -> void
MagicOnion.UnaryResult.UnaryResult(System.Threading.Tasks.Task! rawTaskValue) -> void
MagicOnion.UnaryResult.UnaryResult(System.Threading.Tasks.Task<MagicOnion.Client.IResponseContext<MessagePack.Nil>!>! response) -> void
MagicOnion.UnaryResult.UnaryResult(System.Threading.Tasks.Task<MessagePack.Nil>! rawTaskValue) -> void
MagicOnion.UnaryResult<TResponse>
MagicOnion.UnaryResult<TResponse>.ConfigureAwait(bool continueOnCapturedContext) -> System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResponse>
MagicOnion.UnaryResult<TResponse>.Dispose() -> void
MagicOnion.UnaryResult<TResponse>.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter<TResponse>
MagicOnion.UnaryResult<TResponse>.GetStatus() -> Grpc.Core.Status
Expand Down
131 changes: 67 additions & 64 deletions src/MagicOnion.Abstractions/UnaryResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,53 @@ namespace MagicOnion;
[AsyncMethodBuilder(typeof(AsyncUnaryResultMethodBuilder))]
public readonly struct UnaryResult
{
internal readonly bool hasRawValue;
internal readonly Task? rawTaskValue;
internal readonly Task<IResponseContext<Nil>>? response;
readonly object? value;

[Obsolete("Use default(UnaryResult) instead.")]
public UnaryResult(Nil nil)
{
this.hasRawValue = true;
this.rawTaskValue = default;
this.response = null;
this.value = default;
}

public UnaryResult(Task<Nil> rawTaskValue)
[Obsolete("Use UnaryResult(Task) instead.")]
public UnaryResult(Task<Nil> rawTaskValue) : this((Task)rawTaskValue)
{ }

public UnaryResult(Task<IResponseContext<Nil>> response)
{
this.hasRawValue = true;
this.rawTaskValue = rawTaskValue ?? throw new ArgumentNullException(nameof(rawTaskValue));
this.response = null;
this.value = response ?? throw new ArgumentNullException(nameof(response));
}

public UnaryResult(Task<IResponseContext<Nil>> response)
public UnaryResult(Task rawTaskValue)
{
this.hasRawValue = false;
this.rawTaskValue = default;
this.response = response ?? throw new ArgumentNullException(nameof(response));
this.value = rawTaskValue ?? throw new ArgumentNullException(nameof(rawTaskValue));
}

internal bool HasRawValue
=> this.value is null ||
this.value is Task { IsCompleted: true, IsFaulted: false } and not Task<IResponseContext<Nil>>;

/// <summary>
/// Asynchronous call result.
/// </summary>
public Task ResponseAsync
{
get
{
if (hasRawValue)
// This result has a raw Task value.
if (value?.GetType() == typeof(Task) || value is Task<Nil>)
{
if (rawTaskValue != null)
{
return rawTaskValue;
}
return (Task)value; // Task or Task<Nil>
}
else
{
// If the UnaryResult has no raw-value and no response, it is the default value of UnaryResult.
// So, we will return the default value of TResponse as Task.
if (response is null)
{
return Task.CompletedTask;
}

// This result has a response Task value.
if (value is Task<IResponseContext<Nil>>)
{
return UnwrapResponse();
}

// If the UnaryResult has no raw-value and no response, it is the default value of UnaryResult.
// So, we will return the default value of TResponse as Task.
return Task.CompletedTask;
}
}
Expand All @@ -73,7 +69,7 @@ public Task ResponseAsync
public Task<Metadata> ResponseHeadersAsync => UnwrapResponseHeaders();

Task<IResponseContext<Nil>> GetRequiredResponse()
=> response ?? throw new InvalidOperationException("UnaryResult has no response.");
=> this.value as Task<IResponseContext<Nil>> ?? throw new InvalidOperationException("UnaryResult has no response.");

async Task UnwrapResponse()
{
Expand Down Expand Up @@ -104,6 +100,12 @@ IResponseContext TryUnwrap()
public TaskAwaiter GetAwaiter()
=> ResponseAsync.GetAwaiter();

/// <summary>
/// Configures an awaiter used to await this object.
/// </summary>
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
=> ResponseAsync.ConfigureAwait(continueOnCapturedContext);

/// <summary>
/// Gets the call status if the call has already finished.
/// Throws InvalidOperationException otherwise.
Expand All @@ -130,15 +132,15 @@ public Metadata GetTrailers()
/// </remarks>
public void Dispose()
{
if (response is not null)
if (value is Task<IResponseContext<Nil>> responseTask)
{
if (!response.IsCompleted)
if (!responseTask.IsCompleted)
{
UnwrapDispose();
}
else
{
response.Result.Dispose();
responseTask.Result.Dispose();
}
}
}
Expand Down Expand Up @@ -180,68 +182,63 @@ public static UnaryResult<Nil> Nil
=> new UnaryResult<Nil>(MessagePack.Nil.Default);
}


/// <summary>
/// Represents the result of a Unary call that wraps AsyncUnaryCall as Task-like.
/// </summary>
[AsyncMethodBuilder(typeof(AsyncUnaryResultMethodBuilder<>))]
public readonly struct UnaryResult<TResponse>
{
internal readonly bool hasRawValue; // internal
internal readonly TResponse? rawValue; // internal
internal readonly Task<TResponse>? rawTaskValue; // internal

readonly Task<IResponseContext<TResponse>>? response;
readonly TResponse? rawValue;
readonly object? valueTask;

public UnaryResult(TResponse rawValue)
{
this.hasRawValue = true;
this.rawValue = rawValue;
this.rawTaskValue = null;
this.response = null;
}

public UnaryResult(Task<TResponse> rawTaskValue)
{
this.hasRawValue = true;
this.rawValue = default(TResponse);
this.rawTaskValue = rawTaskValue ?? throw new ArgumentNullException(nameof(rawTaskValue));
this.response = null;
this.valueTask = rawTaskValue ?? throw new ArgumentNullException(nameof(rawTaskValue));
}

public UnaryResult(Task<IResponseContext<TResponse>> response)
{
this.hasRawValue = false;
this.rawValue = default(TResponse);
this.rawTaskValue = null;
this.response = response ?? throw new ArgumentNullException(nameof(response));
this.valueTask = response ?? throw new ArgumentNullException(nameof(response));
}

internal bool HasRawValue
=> this.valueTask is null ||
this.valueTask is Task<TResponse> { IsCompleted: true, IsFaulted: false };

/// <summary>
/// Asynchronous call result.
/// </summary>
public Task<TResponse> ResponseAsync
{
get
{
if (!hasRawValue)
// This result has a raw value.
if (this.valueTask is null)
{
// If the UnaryResult has no raw-value and no response, it is the default value of UnaryResult<TResponse>.
// So, we will return the default value of TResponse as Task.
if (response is null)
{
return Task.FromResult(default(TResponse)!);
}

return UnwrapResponse();
return Task.FromResult(rawValue!);
}
else if (rawTaskValue is not null)

// This result has a raw Task value.
if (this.valueTask is Task<TResponse> t)
{
return rawTaskValue;
return t;
}
else

// This result has a response Task value.
if (valueTask is Task<IResponseContext<TResponse>>)
{
return Task.FromResult(rawValue!);
return UnwrapResponse();
}

// If the UnaryResult has no raw-value and no response, it is the default value of UnaryResult.
// So, we will return the default value of TResponse as Task.
return Task.FromResult(default(TResponse)!);
}
}

Expand All @@ -251,7 +248,7 @@ public Task<TResponse> ResponseAsync
public Task<Metadata> ResponseHeadersAsync => UnwrapResponseHeaders();

Task<IResponseContext<TResponse>> GetRequiredResponse()
=> response ?? throw new InvalidOperationException("UnaryResult has no response.");
=> (valueTask as Task<IResponseContext<TResponse>>) ?? throw new InvalidOperationException("UnaryResult has no response.");

async Task<TResponse> UnwrapResponse()
{
Expand Down Expand Up @@ -294,6 +291,12 @@ IResponseContext<TResponse> TryUnwrap()
public TaskAwaiter<TResponse> GetAwaiter()
=> ResponseAsync.GetAwaiter();

/// <summary>
/// Configures an awaiter used to await this object.
/// </summary>
public ConfiguredTaskAwaitable<TResponse> ConfigureAwait(bool continueOnCapturedContext)
=> ResponseAsync.ConfigureAwait(continueOnCapturedContext);

/// <summary>
/// Gets the call status if the call has already finished.
/// Throws InvalidOperationException otherwise.
Expand All @@ -320,15 +323,15 @@ public Metadata GetTrailers()
/// </remarks>
public void Dispose()
{
if (response is not null)
if (valueTask is Task<IResponseContext<TResponse>> t)
{
if (!response.IsCompleted)
if (!t.IsCompleted)
{
UnwrapDispose();
}
else
{
response.Result.Dispose();
t.Result.Dispose();
}
}
}
Expand Down
36 changes: 15 additions & 21 deletions src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using Grpc.Core;
using MagicOnion.Internal;
using MagicOnion.Server.Internal;
Expand Down Expand Up @@ -37,15 +38,18 @@ public void Bind(IMagicOnionGrpcMethodBinder<TService> binder)

protected static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context)
{
if (result is { hasRawValue: true, rawTaskValue.IsCompletedSuccessfully: true })
if (result.HasRawValue)
{
return Await(result.rawTaskValue, context);
context.Result = BoxedNil;
}
else
{
return Await(result, context);
}
context.Result = BoxedNil;

return default;

static async ValueTask Await(Task task, ServiceContext context)
static async ValueTask Await(UnaryResult task, ServiceContext context)
{
await task.ConfigureAwait(false);
context.Result = BoxedNil;
Expand All @@ -54,28 +58,18 @@ static async ValueTask Await(Task task, ServiceContext context)

protected static ValueTask SetUnaryResult(UnaryResult<TResponse> result, ServiceContext context)
{
if (result.hasRawValue)
if (result.HasRawValue)
{
context.Result = result.GetAwaiter().GetResult();
}
else
{
if (result.rawTaskValue is { } task)
{
if (task.IsCompletedSuccessfully)
{
context.Result = task.Result;
}
else
{
return Await(task, context);
}
}
else
{
context.Result = result.rawValue;
}
return Await(result, context);
}

return default;

static async ValueTask Await(Task<TResponse> task, ServiceContext context)
static async ValueTask Await(UnaryResult<TResponse> task, ServiceContext context)
{
context.Result = await task.ConfigureAwait(false);
}
Expand Down
Loading

0 comments on commit 46d96ac

Please sign in to comment.