Skip to content
Closed
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
44 changes: 43 additions & 1 deletion compiler/cpp/src/thrift/generate/t_netstd_generator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2432,13 +2432,46 @@ void t_netstd_generator::generate_service_client(ostream& out, t_service* tservi
<< indent() << "{" << '\n';
indent_up();

out << indent() << "public Client(TProtocol protocol) : this(protocol, protocol)" << '\n'
out << indent() << "/// <summary>" << '\n'
<< indent() << "/// Initializes a client that reuses a single protocol instance for both directions." << '\n'
<< indent() << "/// </summary>" << '\n'
<< indent() << "/// <remarks>Use this constructor when you already have a single protocol instance and want the client to share it for the lifetime of the client. This is the shared-protocol path; use the transport/factory overloads when you want a fresh protocol pair per call.</remarks>" << '\n'
<< indent() << "/// <param name=\"protocol\">The shared protocol used for both input and output.</param>" << '\n'
<< indent() << "public Client(TProtocol protocol) : this(protocol, protocol)" << '\n'
<< indent() << "{" << '\n'
<< indent() << "}" << '\n'
<< '\n'
<< indent() << "/// <summary>" << '\n'
<< indent() << "/// Initializes a client with separate shared input and output protocols." << '\n'
<< indent() << "/// </summary>" << '\n'
<< indent() << "/// <remarks>Use this constructor when you want a single, long-lived protocol pair for the client. This is the shared-protocol path; the per-call overloads are intended for transports such as <see cref=\"THttpTransport\"/> that can create a fresh protocol pair for each request.</remarks>" << '\n'
<< indent() << "/// <param name=\"inputProtocol\">The shared input protocol used for the lifetime of the client.</param>" << '\n'
<< indent() << "/// <param name=\"outputProtocol\">The shared output protocol used for the lifetime of the client.</param>" << '\n'
<< indent() << "public Client(TProtocol inputProtocol, TProtocol outputProtocol) : base(inputProtocol, outputProtocol)" << '\n'
<< indent() << "{" << '\n'
<< indent() << "}" << '\n'
<< '\n'
<< indent() << "/// <summary>" << '\n'
<< indent() << "/// Initializes a client that reuses a single protocol pair for the shared client and, when the transport supports it, creates a fresh protocol pair for each call." << '\n'
<< indent() << "/// </summary>" << '\n'
<< indent() << "/// <remarks>Use this overload for transports such as <see cref=\"THttpTransport\"/> when you want a per-call protocol pair (for example, to issue overlapping HTTP requests in parallel). If the transport does not support per-call mode, the client continues to use the shared protocol instance.</remarks>" << '\n'
<< indent() << "/// <param name=\"transport\">The shared transport. The client owns protocols created for this transport.</param>" << '\n'
<< indent() << "/// <param name=\"protocolFactory\">The factory used to create input and output protocols for the shared client and, when supported, each call.</param>" << '\n'
<< indent() << "public Client(TTransport transport, TProtocolFactory protocolFactory) : this(transport, protocolFactory, protocolFactory)" << '\n'
<< indent() << "{" << '\n'
<< indent() << "}" << '\n'
<< '\n'
<< indent() << "/// <summary>" << '\n'
<< indent() << "/// Initializes a client with separate factories for the shared client and, when the transport supports it, a fresh protocol pair per call." << '\n'
<< indent() << "/// </summary>" << '\n'
<< indent() << "/// <remarks>Use this overload when the transport supports a per-call protocol path, such as concurrent HTTP calls through <see cref=\"THttpTransport\"/>. The transport may still choose to reuse shared protocols if it does not implement <see cref=\"ITPerCallTransportProvider\"/>.</remarks>" << '\n'
<< indent() << "/// <param name=\"transport\">The shared transport. The client owns protocols created for this transport.</param>" << '\n'
<< indent() << "/// <param name=\"inputProtocolFactory\">The factory used to create the shared input protocol and per-call input protocols when supported.</param>" << '\n'
<< indent() << "/// <param name=\"outputProtocolFactory\">The factory used to create the shared output protocol and per-call output protocols when supported.</param>" << '\n'
<< indent() << "public Client(TTransport transport, TProtocolFactory inputProtocolFactory, TProtocolFactory outputProtocolFactory)" << '\n'
<< indent() << " : base(transport, inputProtocolFactory, outputProtocolFactory)" << '\n'
<< indent() << "{" << '\n'
<< indent() << "}" << '\n'
<< '\n';

vector<t_function*> functions = tservice->get_functions();
Expand All @@ -2454,6 +2487,13 @@ void t_netstd_generator::generate_service_client(ostream& out, t_service* tservi
out << indent() << "public async " << function_signature_async(*functions_iterator, "") << '\n'
<< indent() << "{" << '\n';
indent_up();
out << indent()
<< (((!(*functions_iterator)->is_oneway()) && !(*functions_iterator)->get_returntype()->is_void())
? "return await "
: "await ")
<< "ExecutePerCallAsync(async () =>" << '\n'
<< indent() << "{" << '\n';
indent_up();
out << indent() << "await send_" << function_name << "(";
string call_args = argument_list((*functions_iterator)->get_arglist(),false);
if(! call_args.empty()) {
Expand All @@ -2465,6 +2505,8 @@ void t_netstd_generator::generate_service_client(ostream& out, t_service* tservi
<< "await recv_" << function_name << "(" << CANCELLATION_TOKEN_NAME << ");" << '\n';
}
indent_down();
out << indent() << "}, " << CANCELLATION_TOKEN_NAME << ");" << '\n';
indent_down();
out << indent() << "}" << '\n' << '\n';

// async send
Expand Down
52 changes: 52 additions & 0 deletions lib/netstd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,58 @@ or
- Ensure you have a suitable .NET Core SDK installed, or use the [Ubuntu docker image](../../build/docker/README.md)
- Follow common automake build practice: `./ bootstrap && ./ configure && make`

## Per-call HTTP transport mode

`THttpTransport` supports two modes:

- shared mode: a single protocol pair is reused for the lifetime of the client.
- per-call mode: a fresh protocol pair is created for each request when the client is constructed with a transport and protocol factory.

The shared mode is the default for code such as:

```csharp
var http = new THttpTransport(new Uri("http://localhost:9090"));
var client = new Calculator.Client(new TBinaryProtocol(http));
```

In this form, the same `HttpClient`, default headers, and `ConnectTimeout` remain in effect for the whole client, and the request/response buffers are also reused by the shared protocol instance.

To opt into the per-call path, construct the client with the transport and a protocol factory instead of an already-created protocol instance:

```csharp
var http = new THttpTransport(new Uri("http://localhost:9090"));
var client = new Calculator.Client(http, new TBinaryProtocol.Factory());
```

This constructor enables `ITPerCallTransportProvider` behavior for the generated RPC methods such as `add()`: each method scopes its send and receive operations to a fresh protocol pair for that call. Calling generated `send_add()` and `recv_add()` separately does not use that per-call scope; those methods use the client's shared protocols. Use the regular RPC method when you need per-call behavior.

The per-call path is useful when you want multiple HTTP requests to be in flight at once. Each call gets its own request stream, response stream, and buffered protocol state, while the underlying `HttpClient`, default headers, and `ConnectTimeout` remain shared. In other words:

- shared across calls: `HttpClient`, default headers, `ConnectTimeout`, connection-level settings
- private to each call: request payload, response payload, protocol buffers, per-call `TTransport` state

The same pattern also works when you need a protocol wrapper such as `TMultiplexedProtocol`. Netstd does not provide a built-in `TMultiplexedProtocol.Factory`, so you create a small protocol factory subclass that creates the wrapper for each per-call transport:

```csharp
var http = new THttpTransport(new Uri("http://localhost:9090"));
var client = new Calculator.Client(http, new MultiplexedBinaryProtocolFactory("Calculator"));

sealed class MultiplexedBinaryProtocolFactory : TProtocolFactory
{
private readonly string _serviceName;

public MultiplexedBinaryProtocolFactory(string serviceName)
{
_serviceName = serviceName;
}

public override TProtocol GetProtocol(TTransport trans)
=> new TMultiplexedProtocol(new TBinaryProtocol(trans), _serviceName);
}
```

Socket transports such as `TSocketTransport` are not affected. They do not implement `ITPerCallTransportProvider`, so they continue to use a single shared transport/protocol pair. Wrappers such as `TBufferedTransport` or `TFramedTransport` also disable the per-call path because they are not themselves transport providers.

## Known issues
- In trace logging mode you can see some not important internal exceptions

Expand Down
Loading