Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
46 changes: 32 additions & 14 deletions AustinHarris.JsonRpc.AspNetCore/JsonRpcConnectionHandler.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Threading.Tasks;
using AustinHarris.JsonRpc.Serialization;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;

namespace AustinHarris.JsonRpc.AspNetCore
{
Expand Down Expand Up @@ -35,26 +37,42 @@ private async Task RunAsynchronousMethodsAsync(ConnectionContext connection)
return;
}
reply.Clear();
var pending = JsonRpcProcessor.ProcessAsync(session, document, reply, connection, _options.Serializer, token);
if (!pending.IsCompleted && wrote)
// The document's service scope, when a scoped or transient service is bound: published for
// the duration of the document and disposed asynchronously after its last response is
// written, which is after the running operation has been awaited on every path below.
var scope = _scopes == null ? default : _scopes.CreateAsyncScope();
if (_scopes != null) connection.Features.Set<IServiceProvidersFeature>(new ServiceProvidersFeature { RequestServices = scope.ServiceProvider });
try
{
bool closed = false;
try
var pending = JsonRpcProcessor.ProcessAsync(session, document, reply, connection, _options.Serializer, token);
if (!pending.IsCompleted && wrote)
{
var flush = await output.FlushAsync(token).ConfigureAwait(false);
closed = flush.IsCompleted || flush.IsCanceled;
wrote = false;
bool closed = false;
try
{
var flush = await output.FlushAsync(token).ConfigureAwait(false);
closed = flush.IsCompleted || flush.IsCanceled;
wrote = false;
}
finally
{
// Even a failed flush cannot release the input or reply while invocation runs.
await pending.ConfigureAwait(false);
}
if (closed) return;
}
finally
else await pending.ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (reply.WrittenCount != 0) { reply.CopyTo(output); wrote = true; }
}
finally
{
if (_scopes != null)
{
// Even a failed flush cannot release the input or reply while invocation runs.
await pending.ConfigureAwait(false);
connection.Features.Set<IServiceProvidersFeature>(null);
await scope.DisposeAsync().ConfigureAwait(false);
}
if (closed) return;
}
else await pending.ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (reply.WrittenCount != 0) { reply.CopyTo(output); wrote = true; }
}
if (wrote)
{
Expand Down
49 changes: 47 additions & 2 deletions AustinHarris.JsonRpc.AspNetCore/JsonRpcConnectionHandler.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading.Tasks;
using AustinHarris.JsonRpc.Serialization;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace AustinHarris.JsonRpc.AspNetCore
Expand All @@ -12,14 +15,55 @@ namespace AustinHarris.JsonRpc.AspNetCore
/// back (optionally whitespace / newline separated) and receive responses in order. Wire it up with
/// <c>kestrel.ListenLocalhost(port, l => l.UseConnectionHandler&lt;JsonRpcConnectionHandler&gt;())</c>.
/// The connection's <see cref="ConnectionContext"/> is the RPC context for every call.
/// When a scoped or transient service is bound to the handler's session, each document runs inside one
/// host-owned service scope, published on the connection as <see cref="IServiceProvidersFeature"/> and disposed
/// once the document's response is written; the scope never spans documents.
/// </summary>
public partial class JsonRpcConnectionHandler : ConnectionHandler
{
private readonly JsonRpcOptions _options;
/// <summary>Set only when a scoped or transient service is bound to this handler's session: the cost of a scope per document is opt-in.</summary>
private readonly IServiceScopeFactory _scopes;

public JsonRpcConnectionHandler(IOptions<JsonRpcOptions> options)
public JsonRpcConnectionHandler(IOptions<JsonRpcOptions> options, IServiceProvider services = null)
{
_options = options?.Value ?? new JsonRpcOptions();
_scopes = NeedsDocumentScope(services) ? services.GetService<IServiceScopeFactory>() : null;
}

private bool NeedsDocumentScope(IServiceProvider services)
{
var registrations = services?.GetService<IEnumerable<JsonRpcServiceCollectionExtensions.JsonRpcServiceRegistration>>();
if (registrations == null) return false;
string session = _options.SessionId ?? Handler.DefaultSessionId();
foreach (var r in registrations)
{
if (r.Lifetime == ServiceLifetime.Singleton) continue;
if ((r.SessionId ?? _options.SessionId ?? Handler.DefaultSessionId()) == session) return true;
}
return false;
}

/// <summary>
/// One scope per document: created from <see cref="IServiceScopeFactory"/>, handed to the methods through the
/// connection's <see cref="IServiceProvidersFeature"/> (what the default <see cref="JsonRpcOptions.ServiceProviderSelector"/>
/// reads), removed and disposed once the whole document, batch included, has been answered. Every call of a
/// batch shares it; a transient service is still created per call.
/// </summary>
private void ProcessInScope(string session, in ReadOnlySequence<byte> document, IBufferWriter<byte> output, ConnectionContext connection)
{
using (var scope = _scopes.CreateScope())
{
connection.Features.Set<IServiceProvidersFeature>(new ServiceProvidersFeature { RequestServices = scope.ServiceProvider });
try
{
JsonRpcProcessor.Process(session, in document, output, connection, _options.Serializer);
}
finally
{
connection.Features.Set<IServiceProvidersFeature>(null);
}
}
}

/// <summary>Processes a connection using the hosting mode selected in options.</summary>
Expand Down Expand Up @@ -50,7 +94,8 @@ private async Task RunSynchronousMethodsAsync(ConnectionContext connection)
connection.Abort(new ConnectionAbortedException("JSON-RPC document exceeds MaxRequestBytes."));
return;
}
JsonRpcProcessor.Process(session, in document, output, connection, _options.Serializer);
if (_scopes == null) JsonRpcProcessor.Process(session, in document, output, connection, _options.Serializer);
else ProcessInScope(session, in document, output, connection);
wrote = true;
}
}
Expand Down
22 changes: 22 additions & 0 deletions AustinHarris.JsonRpc.AspNetCore/JsonRpcOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using AustinHarris.JsonRpc.Serialization;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace AustinHarris.JsonRpc.AspNetCore
{
Expand Down Expand Up @@ -28,6 +30,26 @@ public class JsonRpcOptions
/// </summary>
public Func<HttpContext, object> ContextFactory { get; set; }

/// <summary>
/// Finds the <see cref="IServiceProvider"/> that scoped and transient services (see
/// <c>AddJsonRpcService&lt;T&gt;(ServiceLifetime)</c>) are resolved from, given the RPC context of the request
/// (what <see cref="JsonRpcContext.Current"/> returns). Without one, the host handles an <see cref="HttpContext"/>
/// (its <c>RequestServices</c>) and a raw <see cref="ConnectionContext"/> (the scope the connection handler opens
/// per document, published as <see cref="IServiceProvidersFeature"/>). Required when <see cref="ContextFactory"/>
/// produces anything else and a non-singleton service is registered: the host refuses to start, and
/// <c>MapJsonRpc(pattern, options)</c> refuses to map, otherwise. A selector that returns null falls through to
/// the built-in one; when no provider is found the call fails with <c>-32603</c>. The root provider is never used.
/// </summary>
public Func<object, IServiceProvider> ServiceProviderSelector { get; set; }

/// <summary>The built-in selection: the HTTP request's services, or the per-document scope of a raw connection.</summary>
internal static IServiceProvider DefaultServiceProviderSelector(object context)
{
if (context is HttpContext http) return http.RequestServices;
if (context is ConnectionContext connection) return connection.Features.Get<IServiceProvidersFeature>()?.RequestServices;
return null;
}

/// <summary>Largest request body accepted, in bytes. Larger bodies get 413. Default 4 MB.</summary>
public long MaxRequestBytes { get; set; } = 4 * 1024 * 1024;

Expand Down
Loading
Loading